Skip to content

Conversation

@sapphi-red
Copy link
Member

@sapphi-red sapphi-red commented Jul 17, 2025

PIFE is the abbreviation of "Possibly-Invoked Function Expressions". It is a function expression wrapped with a parenthesized expression.
PIFEs annotate functions that are likely to be invoked eagerly. When v8 encounters such expressions, it compiles them eagerly (rather than compiling it later). See v8's blog post for more details.

The blog post only mentions regular FunctionExpressions, but ArrowFunctions are also supported. The cases that will be eagerly compiled are:

  • 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)

Keeping PIFEs as-is in the code generation will unlock optimizations like rolldown/rolldown#5319.

Support by other engines
  • SpiderMonkey (Firefox)
    • when a function expression / arrow function expression is wrapped with parenthesis (code1, code2, code3)
  • JavaScriptCore (Safari)
    • probably not supported

Note: this PR only implements arrow function PIFEs for now.

@graphite-app
Copy link
Contributor

graphite-app bot commented Jul 17, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@github-actions github-actions bot added A-parser Area - Parser A-minifier Area - Minifier A-ast Area - AST A-transformer Area - Transformer / Transpiler A-codegen Area - Code Generation A-formatter Area - Formatter labels Jul 17, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Jul 17, 2025

CodSpeed Instrumentation Performance Report

Merging #12353 will not alter performance

Comparing feat/keep-pifes (0920e98) with main (998c67b)

Summary

✅ 34 untouched benchmarks

@sapphi-red sapphi-red changed the title feat: keep PIFEs feat(codegen): keep PIFEs Jul 17, 2025
@github-actions github-actions bot added the C-enhancement Category - New feature or request label Jul 17, 2025
@sapphi-red
Copy link
Member Author

@oxc-project/core Does this approach make sense? If it does, I'll add pife field to FunctionExpression, too.

@overlookmotel
Copy link
Member

I'm wondering if the additional pife field is necessary.

I assume this alternative approach would also work:

  • Don't change AST or parser.
  • Alter codegen to print parentheses for a ParenthesizedExpression when it contains a Function or ArrowFunctionExpression.
  • Make sure minifier doesn't strip off the parentheses.

Current state of play appears to be:

// PIFEs retained in codegen, functions unwrapped in minifier. Good!
(() => alert(1))();
(() => { alert(2); })();

// PIFEs retained in codegen and minifier,
// but not optimal minification - could unwrap functions
!function() { alert(3); }();
!function() { alert(4); }(), function() { alert(5); }();

// PIFE lost. Bad!
f1 = (function() { alert(6); });
f2 = (() => alert(7));
f3 = (() => { alert(8); });

Playground

So, unless I'm missing something, it looks like the last 3 cases (ParenthesizedExpressions) are the only one we need to correct.

Personally I think it might be preferable not to introduce a pife property for this. It's a duplication of information which is already in AST - which personally I'm keen to avoid, because info in 2 places can get out of sync during transformations.

@Boshen
Copy link
Member

Boshen commented Jul 17, 2025

Make sure minifier doesn't strip off the parentheses.

Parentheses are not supposed to have meanings ... minifier is free to discard it, or it will break.

Parser also has a preserveParentheses: false mode :-)

@Boshen
Copy link
Member

Boshen commented Jul 17, 2025

@oxc-project/core Does this approach make sense? If it does, I'll add pife field to FunctionExpression, too.

Let me think about this.

@sapphi-red
Copy link
Member Author

Yeah, I mainly went with this approach considering preserveParentheses: false.

@Boshen
Copy link
Member

Boshen commented Jul 18, 2025

My thoughts:

  • PIFE is a semantic feature
  • A semantic feature should be encoded in the AST
  • PIFE requires parenthesis, theoretically they can use any other syntax
  • preserveParentheses: false omits parens from the AST
  • DCE and minifier passes removes parens from the AST

@sapphi-red sapphi-red changed the title feat(codegen): keep PIFEs feat(codegen): keep arrow function PIFEs Jul 20, 2025
@sapphi-red sapphi-red marked this pull request as ready for review July 20, 2025 12:55
@Boshen Boshen added the 0-merge Merge with Graphite Merge Queue label Jul 20, 2025
Copy link
Member

Boshen commented Jul 20, 2025

Merge activity

PIFE is the abbreviation of "Possibly-Invoked Function Expressions". It is a function expression wrapped with a parenthesized expression.
PIFEs annotate functions that are likely to be invoked eagerly. When v8 encounters such expressions, it compiles them eagerly (rather than compiling it later). See [v8's blog post](https://v8.dev/blog/preparser#pife) for more details.

The blog post only mentions regular FunctionExpressions, but ArrowFunctions are also supported. The cases that will be eagerly compiled are:

- when `!` comes before function literals ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3730-L3733)) (e.g. `!function(){}`)
- when a function expression is wrapped with parenthesis ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2243-L2248)) (e.g. `(function () {})`, `(async function () {})`)
- when an arrow function is wrapped with parenthesis ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2173-L2174), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2232-L2259), [code3](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3297-L3298)) (e.g. `console.log((() => {}))`)
- when `()` or ``` `` ``` (tagged templates) comes after function literals (only in some cases) ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3987-L3992), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L4344-L4348)) (e.g. `~function(){}()`, ```~function(){}`` ```)
- when [explicit compile hints](https://v8.dev/blog/explicit-compile-hints) are used ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser.cc#L2717-L2720), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L5070-L5073)) (e.g. `//# allFunctionsCalledOnLoad`)

Keeping PIFEs as-is in the code generation will unlock optimizations like rolldown/rolldown#5319.

_Note: this PR only implements arrow function PIFEs for now._
@graphite-app graphite-app bot force-pushed the feat/keep-pifes branch from e67a11e to 0920e98 Compare July 20, 2025 13:13
@graphite-app graphite-app bot merged commit 0920e98 into main Jul 20, 2025
25 checks passed
@graphite-app graphite-app bot deleted the feat/keep-pifes branch July 20, 2025 13:20
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Jul 20, 2025
github-merge-queue bot pushed a commit to rolldown/rolldown that referenced this pull request Jul 20, 2025
This upgrade adds

* oxc-project/oxc#12353
* oxc-project/oxc#12373
* oxc-project/oxc#12393
* oxc-project/oxc#12394
* oxc-project/oxc#12398
* oxc-project/oxc-sourcemap#86

We should expect a small performance improvement when sourcemap is
enabled.
graphite-app bot pushed a commit that referenced this pull request Jul 24, 2025
Added `pife` field to `Function`.

refs #12353
graphite-app bot pushed a commit that referenced this pull request Jul 24, 2025
Made function expression PIFEs to be kept. Similar to #12353 but for function expressions.

The minsize for victory is bigger because webpack's output contains many PIFEs (some shouldn't be written as PIFEs though).

refs #12353
github-merge-queue bot pushed a commit to rolldown/rolldown that referenced this pull request Jul 24, 2025
This PR changes the generated code to use PIFE for callbacks passed to
`__esmMin` wrapper. This improved the start up runtime performance by
2.1x in the case I tested.

I tested this change on
https://github.com/rolldown/benchmarks/tree/main/apps/10000 with the
following config:
```ts
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        advancedChunks: {
          groups: [
            {
              name: 'some'
            }
          ]
        }
      }
    }
  },
  experimental: {
    enableNativePlugin: true,
  },
  esbuild: false,
});
```
I also updated the HTML to measure the render time.
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module">
      globalThis.start = performance.now()
    </script>
    <script type="module" src="./src/index.jsx"></script>
    <script type="module">
      const end = performance.now()
      console.log('render time:', end - globalThis.start)
    </script>
  </body>
</html>
```

The results were (average of 5 times):

|                            | before | after |
|-------------------| ---------- | ------ |
| dev (no minify, hmr=true) | 295.3 ms | 139.7 ms |
| build (minified) | 190.1 ms | 97.2 ms |

I tested this on Chrome 138.0.7204.101.
Also note that this change increases the output code size slightly:

**Before**: 6,338.19 kB (gzip: 1,871.28 kB)
**After**: 6,376.20 kB (gzip: 1,874.64 kB)

requires oxc-project/oxc#12353

---

**Note: The same optimization can be applied to `__commonJSMin`,
`__esm`, `__commonJS`, `createEsmInitializer`.**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ast Area - AST A-codegen Area - Code Generation A-formatter Area - Formatter A-minifier Area - Minifier A-parser Area - Parser A-transformer Area - Transformer / Transpiler C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants