question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Hoist predictable functions calls that are only used once

See original GitHub issue

Under some conditions it is possible to safely replace a function call with the body of the called function.

I believe the following conditions must be met:

  • The function may only be used once.
  • Its use must be a call expression.
  • It may not be called with new.
  • It must be called in either the same scope where it is declared or in a child scope of that.
  • It may not be called with variable arguments (spread or apply).
  • It may not use the arguments object in an unpredicable way, i.e. using loops or passing it around.
  • It may not return conditionally.

I believe this might have a pretty big impact on file size, because:

  • It might further hoist scopes after webpack’s ModuleConcatenationPlugin has done it’s job on a module level, because it would further hoist module level initialization logic.
  • It might remove IIFEs nested inside other IIFEs added by build tools.
  • It might imply the complete removal of functions that have been turned into noops in the build process.

Transforms

noop

in

(() => {
  function noop() {
    // Begin function body
    // End function body
  }

  // More stuff going on here…

  noop();
})()

out

(() => {
  // More stuff going on here…

  // Begin function body
  // End function body
})()

Simple without arguments (Works for arrow functions)

in

(() => {
  let a;

  function fn() {
    // Begin function body
    a++;
    // End function body
  }

  // More stuff going on here…

  fn();
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Begin function body
  a++;
  // End function body
})()

Using a function assignment (Works for arrow functions)

in

(() => {
  let a;

  const fn = fn() {
    // Begin function body
    a++;
    // End function body
  }

  // More stuff going on here…

  fn();
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Begin function body
  a++;
  // End function body
})()

Using a function assignment (Works for arrow functions)

in

(() => {
  let a;

  const fn = () => {
    // Begin function body
    a++;
    // End function body
  }

  // More stuff going on here…

  fn();
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Begin function body
  a++;
  // End function body
})()

Handling function arguments (Works for arrow functions)

in

(() => {
  let a;

  function fn(b, c) {
    // Begin function body
    // Simple function argument
    a += b
    // This is the same as using b
    a *= arguments[0]
    // End function body
  }

  // More stuff going on here…

  fn(42);
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_argument_0 = 42,
      __fn_argument_1;
  // Begin function body
  // Simple function argument
  a += __fn_argument_0
  // This is the same as using b
  a *= __fn_argument_0
  // End function body
})()

Handling function spread params (Works for arrow functions)

in

(() => {
  let a;

  function fn(b, ...params) {
    // Begin function body
    // Simple function argument
    a += b
    // This is the same as using b
    a -= params[0];
    a *= params[1];
    a /= params[2];
    a -= arguments[0];
    a *= arguments[1];
    a /= arguments[2];
    // End function body
  }

  // More stuff going on here…

  fn(1, 2, 3);
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_argument_0 = 1,
      __fn_rest_params = [2, 3];
  // Begin function body
  // Simple function argument
  a += __fn_argument_0
  // This is the same as using b
  a -= __fn_rest_params[0];
  a *= __fn_rest_params[1];
  a /= __fn_rest_params[2];
  a -= __fn_argument_0;
  a *= __fn_rest_params[0];
  a /= __fn_rest_params[1];
  // End function body
})()

Handling additional arguments (Works for arrow functions)

in

(() => {
  let a;

  function fn(b) {
    // Begin function body
    // Simple function argument
    a += b
    // This is the same as using b
    // End function body
  }

  // More stuff going on here…

  fn(1, 2, 3);
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_argument_0 = 1;
  2;
  3;
  // Begin function body
  // Simple function argument
  a += __fn_argument_0
  // This is the same as using b
  // End function body
})()

Arguments reassignment (Works for arrow functions)

in

(() => {
  let a;

  function fn(b) {
    // Begin function body
    // Simple function argument
    b = 1337
    // This is the same as using b
    a *= arguments[0]
    // End function body
  }

  // More stuff going on here…

  fn(42);
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_argument_0 = 42;
  // Simple function argument
  __fn_argument_0 = 1337
  // This is the same as using b
  a *= __fn_argument_0
  // End function body
})()

End return statement (Works for arrow functions)

in

(() => {
  let a;

  function fn() {
    // Begin function body
    return 1337;
    // End function body
  }

  // More stuff going on here…

  a = fn();
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Simple function argument
  // This is the same as using b
  a = 1337;
  // End function body
})()

Handling direct function calls (Works for arrow functions)

in

(() => {
  let a;

  (function(b) {
    // Begin function body
    a += b
    // End function body
  })(6)
})()

out

(() => {
  let a;

  // Function arguments
  let __fn_argument_0 = 6;
  // Begin function body
  a += __fn_argument_0;
  // End function body
})()

Binding this using .call()

in

(() => {
  function fn(b) {
    // Begin function body
    this.b = b;
    // End function body
  }

  // More stuff going on here…

  fn.call({foo: 'bar'}, 'b');
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_this = {foo: 'bar'};
      __fn_argument_0 = 'b';
  // Begin function body
  __fn_this.b = __fn_argument_0;
  // End function body
})()

Binding this using .apply()

in

(() => {
  function fn(b) {
    // Begin function body
    this.b = b;
    // End function body
  }

  // More stuff going on here…

  fn.apply({foo: 'bar'}, ['b']);
})()

out

(() => {
  let a;

  // More stuff going on here…

  // Function arguments
  let __fn_this = {foo: 'bar'};
      __fn_argument_0 = 'b';
  // Begin function body
  __fn_this.b = __fn_argument_0;
  // End function body
})()

Unresolved situations

Using new

(() => {
  function fn() {
    // Begin function body
    // End function body
  }

  // More stuff going on here…

  new fn();
})()

Conditional return

(() => {
  function fn() {
    // Begin function body
    if (condition) {
      return 'so true'
    }
    // Do more stuff
    // End function body
  }

  // More stuff going on here…

  fn();
})()

Variable apply() arguments.

(() => {
  function fn(b) {
    // Begin function body
    this.b = b;
    // End function body
  }

  // More stuff going on here…

  fn.apply({foo: 'bar'}, params);
})()
(() => {
  function fn(b) {
    // Begin function body
    this.b = b;
    // End function body
  }

  // More stuff going on here…

  fn.apply({foo: 'bar'}, ['b', ...params]);
})()

Non standard way of using aruments

(() => {
  function fn(b) {
    // Begin function body
    Array.prototype.map.apply(arguments);
    // End function body
  }

  // More stuff going on here…

  fn();
})()
(() => {
  function fn(b) {
    // Begin function body
    console.log(arguments[i]);
    // End function body
  }

  // More stuff going on here…

  fn();
})()

Corner cases

I believe it is safe to remove assignments to functions if they are used in this simple way.

In

(() => {
  function fn(b) {
    // Begin function body
    // End function body
  }
  
  fn.prop = foo();

  // More stuff going on here…

  fn();
})()

Out

(() => {
  foo();

  // More stuff going on here…

  // Begin function body
  // End function body
})()

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:3
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
remcohaszingcommented, Oct 19, 2017

@qm3ster @j-f1 This would really depend on the function body and how much it could be further optimized.

An obvious case in which the function could be inlined multiple times, is if the function is a no-op.

For other cases it would be hard to determine whether this would make the code longer or shorter.

It could be interesting to eventually transform this:

(() => {
  let x = 0;

  function add(a, b) {
    return a + b;
  }

  x += add(1, 2);
  x += add(3, 4);
})()

Into this:

(() => {
  let x = 0;

  x += 1 + 2;
  x += 3 + 4;
})()

And then this:

(() => {
  let x = 10;
})()

However, until someone finds a solution to determine this, I think it’s better not to inline the function body twice.

2reactions
remcohaszingcommented, Oct 1, 2017

The shadowing variable should be renamed. I’m not very familiar with Babel internals, but I do believe I’ve seen function for creating unique variable names somewhere.

This situation is very similar:

(() => {
  let __fn_argument_0;

  function fn(b) {
  }

  fn();
})()
Read more comments on GitHub >

github_iconTop Results From Across the Web

Understanding Variables, Scope, and Hoisting in JavaScript
This tutorial covers what variables are, how to declare and name them, and also take a closer look at the difference between var,...
Read more >
A Bay Area Man's 1953 'Prophecy' Predicted Smartphones ...
A Bay Area Man's 1953 'Prophecy' Predicted Smartphones, Video Calls and Apple Watches.
Read more >
Optimize Options (Using the GNU Compiler ...
Consider all static functions called once for inlining into their caller even if they are not marked inline . If a call to...
Read more >
caret: Classification and Regression Training
Description Misc functions for training and plotting classification and regression models.
Read more >
typescript spread arguments type
TypeScript Returns a string representing the calling array and its elements. ... If a type parameter is only used once in the function...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found