Skip to content

Instantly share code, notes, and snippets.

@bluwy
Last active October 17, 2023 12:11
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save bluwy/520fe8d8a018078640b1e9d02885243c to your computer and use it in GitHub Desktop.
Save bluwy/520fe8d8a018078640b1e9d02885243c to your computer and use it in GitHub Desktop.
Rollup build optimization research

Rollup build optimization research

Rollup builds doesn't scale well in large apps. You need to increase Node's memory with --max-old-space-size=4096 to handle all the modules. This is one of Vite's highest-rated issue.

This file documents various findings and attempts to improve this issue.

How Rollup works

NOTE: I've only been reading Rollup's source code for a while, so some of these may not be accurate.

High-level flow of Rollup bundling that I find to be performance-sensitive.

  1. Create a module graph for input entries.
    1. Each module has an AST (acorn/estree) after plugin transforms.
    2. Each AST is traversed and re-created as Rollup AST, which nodes are class instances instead of JS objects, each instance hold information for bundling and treeshaking.
  2. Treeshake in an opt-in fashion by detecting what module/exports are used.
    1. This interacts with Rollup's AST, calling nodes recursively to mark itself as "included".
    2. Treeshaking can be done multiple times as "included" nodes can cause more code to be treeshaken. (This operation is cached)
  3. Generate output files.
    1. Does many things, but importantly it "renders" each module by "rendering" the AST.
    2. Each AST node will then rendered itself - the treeshaked output.

Problems and solutions

1. no1 shows that AST is created and duplicated by Rollup again

  • Solution 1: We can fix this by sharing the AST, or removing/unreferencing one of the AST.

    I went with the latter and fixed it at rollup/rollup#4762. It removes the parsed AST (acorn) in favour of re-parsing it when the user requests it again, e.g. moduleInfo.ast. And also applies a LRU cache to make continuous access less taxing.

2. Keeping the AST is expensive

Rollup keeps all module's AST in memory at all times. Recently, I sent a PR to Astro docs optimizing the output code of ~750 MDX files to generate smaller AST, which improved Rollup build time from 90s to 30s. (Previously it even needed 6GB of memory space to not OOM)

  • Solution 1: Similar to my PR, collapse the Rollup AST to not keep a huge tree in memory.

    Not possible as Rollup applies many optimizations recursively. Collapsing ASTs would lose out on it and may make future optimizations harder.

  • Solution 2: Remove unused AST fields.

    Not possible. Rollup uses many fields on the AST and it's not scalable to maintain a list of fields we use. I don't think there's a huge gain either.

  • Solution 3: Create the Rollup AST, do everything we need with it, then quickly tear it down.

    Not possible. As shown of how rollup works, the AST is used throughout the build process and they are stateful, so it can't be tear downed. The only thing we can do ahead of time is whether a module has "effects", but that alone doesn't quite help.

3. Sourcemaps take memory too

Sourcemaps generated for files can be large. However, I haven't looked deep into optimizing how they're handled in Rollup. There are user-reports that disabling sourcemaps in Rollup reduces memory usage though.

What do do

The solutions above may not be not possible, but not feasible without a refactor. So maybe there's a way to acheive the proposed solutions.

But besides this, I hope this document prevents duplicate future work, and allows focusing on alternate optimization approaches instead.

Alternatives

For Vite, there is the idea to switch to esbuild or rolldown.

It's hard to switch to esbuild as Vite's plugin API (which inherits Rollup's API) is not compatible with esbuild. We could write a compatibility layer to run Rollup plugins as esbuild plugins, but it will break things as Rollup's plugin API has a wide surface area. It's also unclear if esbuild's plugin API will allow Vite to do all the tricks it needs to build an optimal output.

rolldown is a compelling option, but it's still a WIP. It will need to do more than Rollup OOTB though to minimize JS <-> binary, which Vite could adapt still.

Another approach Vite's been experimenting is optimizing dependencies in builds. Theoretically, if all deps are pre-bundled, it would result in lesser code to be parsed. There's still rough edges to solve though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment