Skip to content

refactor: store safe variable names in cache for subsequent usage#5947

Merged
lukastaegert merged 23 commits into
rollup:masterfrom
Aslemammad:feat/safeVariableNames
Nov 7, 2025
Merged

refactor: store safe variable names in cache for subsequent usage#5947
lukastaegert merged 23 commits into
rollup:masterfrom
Aslemammad:feat/safeVariableNames

Conversation

@Aslemammad

@Aslemammad Aslemammad commented May 4, 2025

Copy link
Copy Markdown
Contributor

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:

Description

While testing the 10000 vite benchmark example I found out the main bottleneck was the deconflictTopLevelVariables function (avoids using the same name for variables when all the chunks get merged together). Even after introducing the cache property which required few changes on the vite side, still that caused a small performance improvement (1s-2s).

deconflictTopLevelVariables shines through the flamegraph

After digging in, I came to find that the cache was only being used in the bundle generation step and not in the generate or write step where deconflictTopLevelVariables -> getSafeName sits.

The idea here is to reduce the usage of getSafeName by caching the already generated safe names by the same function. The new cache field is simply named safeVariableNames which is an object. This results in a 2x performance improvement (12s -> 5s).

12s -> 5s improvment

Teams can achieve a way better build performance specially in CI/CDs (e.g. deploys) once they start using the cache property on rollup.

@vercel

vercel Bot commented May 4, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
rollup Ready Ready Preview Comment Nov 7, 2025 1:49pm

Comment thread src/rollup/rollup.ts
@Aslemammad Aslemammad marked this pull request as ready for review May 4, 2025 17:44

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the interesting impulse. This is definitely something to consider looking into. Nevertheless, the current approach has some issues. I added a test to demonstrate the most severe one: When a module is changed in a way that would produce a name conflict, the names in the updated module are no longer deconflicted, resulting in an invalid bundle.
In general, we have an issue with reproducibility: With this change, the result no longer depends on the input alone but also on the name cache. If top level variables change in one module (or even just the import order), a fresh build might deconflict differently than a build using the cache. I wonder if it is possible to work around this in some way.

@Aslemammad

Copy link
Copy Markdown
Contributor Author

hey! thank you so much for the review.

i'll look into this and try to resolve it. is it just the test case need to be resolved, or we have to inspect the whole general issue? if it's the latter, i'd appreciate some pointers that i should look into since my knowledge of the codebase is pretty limited.

but anyway, i'll try to figure this out soon.

@lukastaegert

Copy link
Copy Markdown
Member

I think it is a more general issue, unfortunately I do not see a simple solution yet. Another problem is if the accessed global variables change in a module, which can influence deconflicting in all other modules. Maybe one needs to figure out a performant way to check if the accessed top-level and global symbols changed in a module.

Comment thread src/utils/deconflictChunk.ts Outdated
@Aslemammad

Copy link
Copy Markdown
Contributor Author

@lukastaegert hey! i just fixed the tests. it was because of a special case where the name was toString and it was referencing the Object.toString rather than returning undefined.

I also resolved the order issue we talked about earlier by only adding to usedNames iteratively rather than all in once.

@Aslemammad

Aslemammad commented Jun 26, 2025

Copy link
Copy Markdown
Contributor Author

sorry i just closed this by misclicking the close with comment button. re-opened now!

@github-actions

github-actions Bot commented Jul 12, 2025

Copy link
Copy Markdown

Thank you for your contribution! ❤️

You can try out this pull request locally by installing Rollup via

npm install Aslemammad/rollup#feat/safeVariableNames

Notice: Ensure you have installed the latest nightly Rust toolchain. If you haven't installed it yet, please see https://www.rust-lang.org/tools/install to learn how to download Rustup and install Rust.

or load it into the REPL:
https://rollup-dlh7ovv8k-rollup-js.vercel.app/repl/?pr=5947

@github-actions

github-actions Bot commented Jul 12, 2025

Copy link
Copy Markdown

Performance report

  • BUILD: 7040ms, 814 MB
    • initialize: 0ms, 24.9 MB (+8%)
    • generate module graph: 2609ms, 628 MB
      • generate ast: 1372ms (-74ms, -5.1%), 620 MB
    • sort and bind modules: 414ms, 681 MB
    • mark included statements: 3992ms, 814 MB
      • treeshaking pass 1: 2341ms, 811 MB
      • treeshaking pass 2: 466ms, 811 MB
      • treeshaking pass 3: 399ms, 815 MB
      • treeshaking pass 4: 387ms, 816 MB (+3%)
      • treeshaking pass 5: 384ms, 814 MB
  • GENERATE: 688ms (-56ms, -7.5%), 908 MB (-5%)
    • initialize render: 0ms, 814 MB (-14%)
    • generate chunks: 48ms (-196ms, -80.4%), 814 MB (-7%)
      • optimize chunks: 0ms, 832 MB (-7%)
    • render chunks: 623ms, 882 MB (-5%)
    • transform chunks: 18ms, 908 MB (-5%)
    • generate bundle: 0ms, 908 MB (-5%)

Comment thread test/chunking-form/index.js Outdated
@lukastaegert lukastaegert force-pushed the feat/safeVariableNames branch from acac6b6 to e3733c0 Compare October 25, 2025 13:01
Comment thread src/utils/deconflictChunk.ts
Comment thread src/utils/deconflictChunk.ts

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, from my side I am really happy now with how this turned out. And I am really sorry for the extremely long wait. There were many other things going on on my side and maybe you understand that I was somewhat afraid of this PR and its consequences. However, I think this solution is pretty solid and also future-proof now. Also, the performance report already shows how this speeds up bundling Rollup itself, which is amazing.

Please have another look. If you are fine and have no more comments, we can release this now.

@lukastaegert lukastaegert added this pull request to the merge queue Nov 7, 2025
Merged via the queue into rollup:master with commit 05a6c01 Nov 7, 2025
45 checks passed
@github-actions

github-actions Bot commented Nov 7, 2025

Copy link
Copy Markdown

This PR has been released as part of rollup@4.53.0. You can test it via npm install rollup.

@Aslemammad

Aslemammad commented Dec 10, 2025

Copy link
Copy Markdown
Contributor Author

hey @lukastaegert, thank you so much for the merge and sorry for the reply! work on my side has been hectic.

this literally shined my week and i can't believe my eyes. thank you so much for the merge.

@Aslemammad Aslemammad deleted the feat/safeVariableNames branch December 10, 2025 15:13
@Aslemammad

Copy link
Copy Markdown
Contributor Author

i'll hopefully write a blog post on thundraa.com, and if you think it'd be appropriate, i'd be happy to host it on the rollup blog/docs instead, let me know.

@tariqrafique

Copy link
Copy Markdown
Contributor

@Aslemammad unfortunately your change broke one of our use cases.
I've validated that it was broken by this commit. Details in #6357

For a minor version bump this feels like a breaking change.

tariqrafique added a commit to tariqrafique/rollup that referenced this pull request Apr 17, 2026
PR rollup#5947 added "exports" to the universal RESERVED_NAMES set, which
caused every variable or parameter named "exports" to be renamed to
"exports$1" in any scope, even when no conflict exists with the output
format's runtime "exports" identifier. Because eval strings are not
rewritten, this silently broke code like:

    (unused, exports) => {
      console.log(exports.bar);
      eval('exports.bar = 1');
    }

where the parameter was renamed but the eval argument was not. The same
over-aggressive rename also applied to unrelated nested bindings in ES
output, which has no runtime "exports" at all.

This change removes "exports" from RESERVED_NAMES and restores the
system-format-specific check in ChildScope.addUsedOutsideNames so that
nested scopes in SystemJS output still treat "exports" as used when
they reference an outer exported binding. Top-level protection for
CJS/AMD/UMD/IIFE/SYSTEM output is unchanged: RESERVED_USED_NAMES in
Chunk.ts still seeds the chunk-level usedNames with "exports", and
reassigned exports still carry renderBaseName === "exports" which
propagates into nested scopes via getBaseVariableName().

The deconflictTopLevelVariables cache from PR rollup#5947 is untouched, so
its performance benefits are preserved; a benchmark that reliably sees
the PR's generate-time speedup (~20% vs pre-PR) shows no regression
from this change.

Fixes rollup#6357

Made-with: Cursor
pull Bot pushed a commit to LeeeeeeM/rollup that referenced this pull request May 3, 2026
…llup#6360)

PR rollup#5947 added "exports" to the universal RESERVED_NAMES set, which
caused every variable or parameter named "exports" to be renamed to
"exports$1" in any scope, even when no conflict exists with the output
format's runtime "exports" identifier. Because eval strings are not
rewritten, this silently broke code like:

    (unused, exports) => {
      console.log(exports.bar);
      eval('exports.bar = 1');
    }

where the parameter was renamed but the eval argument was not. The same
over-aggressive rename also applied to unrelated nested bindings in ES
output, which has no runtime "exports" at all.

This change removes "exports" from RESERVED_NAMES and restores the
system-format-specific check in ChildScope.addUsedOutsideNames so that
nested scopes in SystemJS output still treat "exports" as used when
they reference an outer exported binding. Top-level protection for
CJS/AMD/UMD/IIFE/SYSTEM output is unchanged: RESERVED_USED_NAMES in
Chunk.ts still seeds the chunk-level usedNames with "exports", and
reassigned exports still carry renderBaseName === "exports" which
propagates into nested scopes via getBaseVariableName().

The deconflictTopLevelVariables cache from PR rollup#5947 is untouched, so
its performance benefits are preserved; a benchmark that reliably sees
the PR's generate-time speedup (~20% vs pre-PR) shows no regression
from this change.

Fixes rollup#6357

Made-with: Cursor

Co-authored-by: Lukas Taegert-Atkinson <lukastaegert@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants