Minify: Hoist predictable functions calls that are only used once

Created on 1 Oct 2017  Ā·  6Comments  Ā·  Source: babel/minify

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
})()
enhancement

Most helpful comment

@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.

All 6 comments

Those look like great optimizations! Just 1 question for you:

// What should this transform to?
(() => {
  let a;
  function fn() {
    let a = 2; // shadowing!
  }
  fn();
})

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();
})()

The function may only be used once.

If it is used twice, we can just inline it twice! :stuck_out_tongue_winking_eye:

@qm3ster But then it would be longer, since we’re inserting the function body twice, instead of just once as happens without inlining.

@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.

Comment in passing - the requirements in the OP say 'It may not return conditionally', but that's easy enough to deal with:

function f() {
  if (Math.random() < .5) return;
  console.log('hit');
}
f();

->

f : {
  if (Math.random() < .5) break f;
  console.log('hit');
}

'Course, this probably isn't worth worrying about for the moment. Though that's true also for a lot of the weirder cases - to get this started, e.g., I'd strongly suggest ignoring all functions which use this or arguments at all, or which are invoked with .call.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zamb3zi picture zamb3zi  Ā·  3Comments

boopathi picture boopathi  Ā·  3Comments

EladBezalel picture EladBezalel  Ā·  4Comments

jamesloewen picture jamesloewen  Ā·  3Comments

phoxer picture phoxer  Ā·  3Comments