Skip to content

Preserve parenthesis around function expressions #4252

Description

@sapphi-red

Parenthesized function expressions (e.g. (function () {}), (() => {})) are treated slightly differently from non-parenthesized ones by v8 and SpiderMonkey. These are considered by the engines as likely to be invoked immediately and are complied eagerly (while normal functions are compiled lazily). See v8's blog post for more details.
While this has the same affect as #4247, it is supported by older browsers and also supported by Firefox (AFAIK Safari doesn't). Also it has more granular control over which function to apply to.

Currently esbuild strips these parentheses (esbuild try). So even if I intentionally write them in the source code, they are removed in the output.

The conditions under which engines treat functions as likely to be invoked eagerly are:

  • v8 (Chromium)
    • when ! comes before function literals (code) (e.g. !function(){})
    • when a function expression is wrapped with parenthesis (code) (e.g. (function () {}), (async function () {}))
    • when an arrow function is wrapped with parenthesis (code1, code2, code3) (e.g. console.log((() => {})))
    • when () or `` (tagged templates) comes after function literals (only in some cases) (code1, code2) (e.g. ~function(){}(), ~function(){}`` )
    • when explicit compile hints are used (code1, code2) (e.g. //# allFunctionsCalledOnLoad)
  • SpiderMonkey (Firefox)
    • when a function expression / arrow function expression is wrapped with parenthesis (code1, code2, code3)

As a side note, this technique can also be applied to esbuild's bundle output. For example,

// omitted helpers

// bar1.mjs
var bar1_exports = {};
__export(bar1_exports, {
  default: () => bar1_default
});
var bar1_default;
var init_bar1 = __esm({
  "bar1.mjs"() {
    console.log("bar1");
    bar1_default = "bar1";
  }
});

// foo.cjs
var require_foo = __commonJS({
  "foo.cjs"(exports, module) {
    module.exports = {
      bar1: (init_bar1(), __toCommonJS(bar1_exports))
    };
  }
});

// entry.mjs
var import_foo = __toESM(require_foo(), 1);
console.log(import_foo.default);

…can be optimized to:

// omit helpers

// bar1.mjs
var bar1_exports = {};
__export(bar1_exports, {
  default: () => bar1_default
});
var bar1_default;
var init_bar1 = __esm({
  "bar1.mjs": (() => {
    console.log("bar1");
    bar1_default = "bar1";
  })
});

// foo.cjs
var require_foo = __commonJS({
  "foo.cjs": (exports, module) => ({
    module.exports = {
      bar1: (init_bar1(), __toCommonJS(bar1_exports))
    };
  })
});

// entry.mjs
var import_foo = __toESM(require_foo(), 1);
console.log(import_foo.default);

..., given that these callbacks will be called in the initial load. There's a chance that require is called lazily though.
esbuild try
(related: rolldown/rolldown#5319)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions