perf(renamer): optimize symbol renaming by eliminating rename_non_root_symbol pass#7867
perf(renamer): optimize symbol renaming by eliminating rename_non_root_symbol pass#7867shulaoda merged 24 commits intorolldown:mainfrom
rename_non_root_symbol pass#7867Conversation
There was a problem hiding this comment.
Pull request overview
This PR optimizes the symbol renaming algorithm by eliminating the expensive rename_non_root_symbol pass that previously iterated through all nested scope symbols in parallel. The optimization achieves ~5-7% performance improvement on representative benchmarks (threejs, rome_ts).
Changes:
- Rewrites
add_symbol_in_root_scopeto check for conflicts with nested scope symbols upfront during root scope renaming - Adds entry module tracking and name availability checks to avoid conflicts
- Replaces expensive parallel
rename_non_root_symboliteration with lightweightregister_nested_scope_symbolsfor edge cases only - Removes
with_scope_tree_child_idsparameter from semantic analysis as it's no longer needed
Reviewed changes
Copilot reviewed 57 out of 57 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/rolldown/src/utils/renamer.rs | Core algorithm rewrite - adds entry module tracking, name availability checks, rewrites add_symbol_in_root_scope, replaces rename_non_root_symbol with register_nested_scope_symbols |
| crates/rolldown/src/utils/pre_process_ecma_ast.rs | Removes with_scope_tree_child_ids parameter from recreate_scoping calls |
| crates/rolldown/src/utils/chunk/deconflict_chunk_symbols.rs | Updates to use new renamer API, adds nested scope symbol registration loop |
| crates/rolldown/src/stages/generate_stage/mod.rs | Removes pre-computation of module_scope_symbol_id_map |
| Test snapshots | Hash and variable name changes reflecting the new naming algorithm |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I found some test cases regressed(It was not renamed before, but it has been renamed in this pr). Can you double-check it? |
Thank you for reviewing this PR quickly! I just let Claude code to make a POC for #4014 to see if it still has any performance improvements at this time, and I'm not deciding whether to continue or not. |
|
Open it to trigger the benchmark. |
No worries! Thanks for exploring this |
Merging this PR will improve performance by 3.9%
Performance Changes
Comparing |
Response to Copilot ReviewAfter careful analysis, all three of Copilot's suggestions are based on misunderstandings of the code logic: Comment 1 (Reserved name check at line 257)The current logic is correct. The condition
Copilot's suggestion is a structural refactoring that doesn't change the behavior - it's just a style preference. Comment 2 (Inconsistency at line 193)The "inconsistency" is intentional and documented. See lines 81-82: // For entry module, pre-cache all symbol names (both root and non-root)
// to ensure we don't rename other symbols to names that exist in the entry moduleAdditionally, if self.entry_module_idx.is_none_or(|entry_idx| canonical_ref.owner != entry_idx) {
let _ = self.get_or_create_module_used_names(canonical_ref.owner);
}This explicitly guards against calling it for the entry module. Comment 3 (RUNTIME_MODULE_INDEX at line 172)Using a named constant
|
|
FYI, we have a criterion-based benchmark (this requires you to clone the repo directly rather than from a forked repo; IIRC, this is a security concern). |
e9f925e to
a8ca640
Compare
|
Great! I’ve been waiting for the PR to come back. It feels like this isn’t just a performance improvement — the naming strategy also seems closer to Rollup now (i.e. without unnecessary or redundant renaming). |
eabc9be to
6d14944
Compare
6d14944 to
d52cd14
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 38 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
32d918f to
92dca8f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a2cae7f to
eb12728
Compare
5e918bf to
f5f77b3
Compare
✅ Deploy Preview for rolldown-rs canceled.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 39 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Head branch was pushed to by a user without write access
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 39 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
## Summary Optimize the nested scope symbol renaming pass by only processing symbols whose names match a top-level canonical name in the same module. - Collect canonical names of top-level symbols into a HashSet - Only call `register_nested_scope_symbols` for nested symbols whose names match a canonical name (or for CJS wrapped modules) - Nested symbols that don't match any canonical name can safely shadow cross-module top-level symbols via JavaScript's natural scoping rules This is a follow-up optimization to #7867. Also partially fixes #6586 (comment) ## Test plan - [x] Added new test case `nested_scope_same_module_conflict` to verify the optimization behavior - [x] Updated `nested_scope_rename_counter` test with improved comments - [x] All existing tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Dunqing <[email protected]> Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]> Co-authored-by: Yunfei He <[email protected]> Co-authored-by: dalaoshu <[email protected]>
Note: waiting for oxc-project/oxc#18053 to merge to unblock this (Edit: merged, and already released) ## Summary Fixes: #6586 This PR refines the symbol renaming (deconflict) logic to correctly handle both **top-level symbol conflicts** and **nested scope shadowing**. The key insight is that these are two distinct problems requiring different solutions. Related fixes: - #7859 - #7867 - #7899 This PR essentially reverts all the logic implemented in the previous fixes while preserving the performance improvements introduced in #7867 (4%). --- ## 1. Top-Level Symbol Renaming When bundling multiple modules into a single chunk, symbols with the same name need unique identifiers. **Example:** ```js // lib.js export const value = 1; // other.js export const value = 2; // main.js import { value } from './lib.js'; import { value as otherValue } from './other.js'; console.log(value, otherValue); ``` **Output:** ```js const value = 1; // lib.js - keeps original name (entry module priority) const value$1 = 2; // other.js - renamed to avoid conflict console.log(value, value$1); ``` **Rules:** - Entry module symbols get naming priority - Subsequent conflicting symbols get suffixed: `$1`, `$2`, etc. - Reserved names (keywords, globals like `Object`, `Promise`) are always avoided --- ## 2. Nested Scope Symbol Renaming This handles cases where a nested binding (function parameter, catch clause, block-scoped variable) interacts with top-level symbols. ### Case A: NO renaming needed - Intentional shadowing When a nested binding shadows an import that **kept its original name**, no renaming is needed because JavaScript's scoping rules handle it correctly. **Example (`preserve_shadowing_param_name`):** ```js // lib.js export const Client = []; // main.js import { Client } from './lib.js'; // This param shadows the imported `Client`, but should NOT be renamed // since shadowing is intentional and doesn't cause conflicts at runtime. const Config = (Client) => { console.log(Client); // → refers to parameter }; console.log(Client); // → refers to import ``` **Output:** ```js const Client = []; const Config = (Client) => { // Parameter keeps its name ✓ console.log(Client); }; console.log(Client); ``` --- ### Case B: Star import member references - Renaming needed When a namespace import member (like `ns.foo`) is referenced inside a function, and a nested binding would capture that reference, the nested binding must be renamed. **Example (`argument-treeshaking-parameter-conflict`):** ```js // dep.js export let value = 0; export const mutate = () => value++; // main.js import * as dep from './dep'; function test(mutate) { // Parameter named 'mutate' dep.mutate('hello'); // Reference to dep.mutate (top-level) } test(); assert.strictEqual(dep.value, 1); ``` **Output:** ```js let value = 0; const mutate = () => value++; function test(mutate$1) { // Parameter renamed to mutate$1 mutate("hello"); // Correctly calls top-level mutate } test(); assert.strictEqual(value, 1); ``` **Why renaming is needed:** 1. The namespace import `dep.mutate` resolves to the top-level `mutate` function 2. Inside `test`, the parameter `mutate` would shadow the top-level `mutate` 3. The reference `dep.mutate('hello')` becomes just `mutate("hello")` after bundling 4. Without renaming the parameter, this call would incorrectly invoke the parameter instead of the top-level function 5. Solution: Rename the parameter to `mutate$1` so the reference correctly resolves to the top-level `mutate` --- ### Case C: Named imports - Renaming needed When a named import is renamed due to a top-level conflict, and a nested binding has the same name as the **renamed** import, that nested binding must be renamed to avoid capturing references. **Example (`basic_scoped`):** ```js // a.js const a = 'a.js'; export { a }; // main.js import { a as aJs } from './a'; const a = 'main.js'; // Local 'a' takes priority (entry module) function foo(a$1) { // Parameter named 'a$1' return [a$1, a, aJs]; // References: param, local, import } assert.deepEqual(foo('foo'), ['foo', 'main.js', 'a.js']); ``` **Output:** ```js const a$1 = "a.js"; // Import renamed (conflicts with local 'a') const a = "main.js"; // Local keeps name (entry module priority) function foo(a$1$1) { // Parameter renamed to a$1$1 return [a$1$1, a, a$1]; // Correctly resolves all three references } assert.deepEqual(foo("foo"), ["foo", "main.js", "a.js"]); ``` **Why renaming is needed:** 1. `a` from `a.js` is renamed to `a$1` (conflicts with local `a` in entry module) 2. The alias `aJs` resolves to the renamed `a$1` 3. Inside `foo`, the parameter `a$1` would capture the reference to `aJs` 4. Solution: Rename the parameter to `a$1$1` so `aJs` correctly resolves to the top-level `a$1` --- > **Technical Note:** Cases B and C use the same underlying mechanism to detect shadowing: > > 1. Get the `reference_id` from the reference site (where the symbol is used) > 2. Use `scoping.scope_ancestors(reference.scope_id())` to walk up the scope chain > 3. Check if any ancestor scope has a binding with the same name as the renamed symbol > 4. If found, rename that nested binding to avoid capture > > This detection relies on oxc-project/oxc#18053 which added `scope_id` to `Reference`, enabling us to locate the exact scope where the reference occurs. --- ### Case D: CJS wrapper parameters - Renaming needed For CommonJS wrapped modules, nested scopes must avoid shadowing the synthetic `exports` and `module` parameters injected by the CJS wrapper. **Example:** ```js // cjs-module.js (detected as CommonJS) function helper() { const exports = {}; // Would shadow CJS wrapper's exports parameter return exports; } module.exports = helper; ``` **Output:** ```js var require_cjs_module = __commonJS((exports, module) => { function helper() { const exports$1 = {}; // Renamed to avoid shadowing return exports$1; } module.exports = helper; }); ``` --- ## Implementation Details The renaming happens in two phases: 1. **`add_symbol_in_root_scope`** - Assigns canonical names to top-level symbols, checking: - Is the name already used by another top-level symbol? - For facade symbols (external module namespaces): would it conflict with nested scopes in the entry module? 2. **`NestedScopeRenamer`** - Renames nested bindings that would capture references: - **`rename_bindings_shadowing_star_imports`** (Case B): Star import member references (`ns.foo`) - **`rename_bindings_shadowing_named_imports`** (Case C): Named imports that were renamed - **`rename_bindings_shadowing_cjs_params`** (Case D): CJS wrapper parameter shadowing (`exports`, `module`) --- ## Test Cases | Test | Case | Description | |------|------|-------------| | `preserve_shadowing_param_name` | A | Parameter shadows import, keeps its name (intentional) | | `argument-treeshaking-parameter-conflict` | B | Namespace member ref, parameter renamed | | `basic_scoped` | C | Named import renamed, nested binding renamed | | `cjs` | D | CJS wrapper parameter shadowing | | Rollup: `preserves-catch-argument` | A | Catch parameter shadows import, keeps its name | | Rollup: `class-name-conflict-2` | A | Class in block scope, keeps its name | > Note for reviewer: Code comments and PR description are almost generated by Claude Code, but with careful self-review.
## [1.0.0-rc.1] - 2026-01-22 ### 🚀 Features - debug_info: add facade chunk elimination reason (#7980) by @IWANABETHATGUY - support lazy barrel optimization (#7933) by @shulaoda - add `experimental.lazyBarrel` option (#7908) by @shulaoda - skip unused external modules from IIFE parameter list (#7978) by @sapphi-red - add custom panic hook for better crash reporting (#7752) by @shulaoda - treeshake: add `invalidImportSideEffects` option (#7958) by @shulaoda - merge allow-extension emitted chunks (#7940) by @IWANABETHATGUY - nativeMagicString generateMap (#7944) by @IWANABETHATGUY - Include meta.magicString in RenderChunkMeta (#7943) by @IWANABETHATGUY - debug_info: add debug info for eliminated facade chunks (#7946) by @IWANABETHATGUY - stablize `strictExecutionOrder` and move to `output.strictExecutionOrder` from `experimental.strictExecutionOrder` (#7901) by @sapphi-red - add documentation link to require() error message (#7898) by @Copilot - add `codeSplitting: boolean` and deprecate `inlineDynamicImports` (#7870) by @hyf0 - dev: change lazy module URL to `/@vite/lazy` from `/lazy` (#7884) by @sapphi-red ### 🐛 Bug Fixes - transform JS files containing `</script>` to escape template literals (#7987) by @IWANABETHATGUY - apply avoid-breaking-exported-api = false to clippy.toml and fix clippy errors (#7982) by @Boshen - pass `kind` from `this.resolve` (#7981) by @sapphi-red - rolldown_plugin_vite_resolve: ignore yarn resolution errors and fallback to other resolvers (#7968) by @sapphi-red - renamer: prevent renaming symbols when there no conflicts (#7936) by @Dunqing - correct minifyInterExports when emitted chunk got merged (#7941) by @IWANABETHATGUY - deduplicate entry points when module is both emitted and dynamically imported (#7885) by @IWANABETHATGUY - dev: add `@vite-ignore` to lazy compilation proxy module import (#7883) by @sapphi-red ### 🚜 Refactor - rust: enable clippy nursery lint group (#8002) by @Boshen - rust: fix inconsistent_struct_constructor clippy lint (#7999) by @Boshen - rust: fix needless_pass_by_ref_mut clippy lint (#7994) by @Boshen - rust: fix unnecessary_wraps clippy lint (#7993) by @Boshen - rust: fix enum_variant_names clippy lint (#7992) by @Boshen - fix single_match clippy lint (#7997) by @Boshen - rust: fix redundant_clone clippy lint (#7996) by @Boshen - rust: rename CJS to Cjs to follow upper_case_acronyms lint (#7991) by @Boshen - rust: remove unnecessary Box wrapper around Vec in MemberExprRef (#7990) by @Boshen - import_record: make resolved_module optional (#7907) by @shulaoda - remove unnecessary `.parse` (#7966) by @sapphi-red - remove unused `ImportRecordMeta::IsPlainImport` (#7948) by @shulaoda - proper set chunk meta (#7939) by @IWANABETHATGUY - module_loader: remove `try_spawn_with_cache` (#7920) by @shulaoda - link_stage: simplify `ImportStatus::NoMatch` to unit variant (#7909) by @shulaoda - improve global scope symbol reservation in chunk deconfliction (#7906) by @IWANABETHATGUY - simplify ast unwrapping in generate stage (#7900) by @IWANABETHATGUY - generate_stage: optimize cross-chunk imports computation (#7889) by @shulaoda - link_stage: move runtime require logic into match branch (#7892) by @shulaoda - link_stage: simplify runtime require reference conditions (#7891) by @shulaoda - link_stage: inline and simplify external dynamic import check (#7890) by @shulaoda - generate_stage: simplify external module import collection logic (#7887) by @shulaoda - avoid redundant module lookup in TLA computation (#7886) by @shulaoda - dev: `devEngine.compileEntry` does not return null (#7882) by @sapphi-red - dev: fix type errors for test HMR runtime (#7881) by @sapphi-red - dev: move `clientId` property to `DevRuntime` base class (#7880) by @sapphi-red - dev: generate client id in browser (#7878) by @hyf0 ### 📚 Documentation - apis: organize hook filters documentation and add composable filters section (#8003) by @sapphi-red - update `vitepress-plugin-group-icons` (#7947) by @yuyinws - add in-depth documentation for lazy barrel optimization (#7969) by @shulaoda - bump theme & update activeMatch for reference (#7963) by @mdong1909 - mark `build()` API as experimental (#7954) by @sapphi-red - enhance search functionality with improved scoring and filtering logic (#7935) by @hyf0 - add minor comments to multiple types (#7930) by @sapphi-red - refactor advanedChunks related content to adapt manual code splitting concept (#7925) by @hyf0 - apis: add content to Bundler API page (#7926) by @sapphi-red - apis: restructure plugin API related docs (#7924) by @sapphi-red - add plugin API docs (#7923) by @sapphi-red - apis: add docs to important APIs (#7913) by @sapphi-red - move the important APIs to the top of the sidebar (#7912) by @sapphi-red - apis: add more content to CLI documentation (#7911) by @sapphi-red - apis: generate CLI docs from --help output (#7910) by @sapphi-red - add fathom analytics (#7896) by @mdong1909 ### ⚡ Performance - use u32 for string indices in string_wizard and rolldown to reduce memory usage (#7989) by @IWANABETHATGUY - rust: remove all usages of `with_scope_tree_child_ids(true)` for `SemanticBuilder` (#7995) by @Dunqing - renamer: skip unnecessary nested scope symbol processing (#7899) by @Dunqing - module_loader: use ArcStr for importer_id to avoid string copy (#7922) by @shulaoda - module_loader: defer `ModuleTaskOwner` construction until needed (#7921) by @shulaoda - renamer: optimize symbol renaming by eliminating `rename_non_root_symbol` pass (#7867) by @Dunqing ### 🧪 Testing - add lazy barrel optimization test cases (#7967) by @shulaoda ### ⚙️ Miscellaneous Tasks - remove lazy barrel option (#8010) by @shulaoda - mark watch API as experimental (#8004) by @sapphi-red - deps: update dependency lodash-es to v4.17.23 [security] (#8001) by @renovate[bot] - git ignore zed local config (#7988) by @IWANABETHATGUY - setup publint for published packages (#7972) by @Copilot - enable `tagged_template_transform ` uncondionally (#7975) by @IWANABETHATGUY - deps: update oxc to v0.110.0 (#7964) by @renovate[bot] - deps: update oxc apps (#7962) by @renovate[bot] - ai: add upgrade-oxc Claude skill (#7957) by @Boshen - deps: update rollup submodule for tests to v4.55.2 (#7959) by @sapphi-red - deps: update test262 submodule for tests (#7960) by @sapphi-red - deps: update crate-ci/typos action to v1.42.1 (#7961) by @renovate[bot] - deps: update rust crates (#7951) by @renovate[bot] - deps: update npm packages (#7953) by @renovate[bot] - deps: update github-actions (#7952) by @renovate[bot] - deps: update npm packages (#7950) by @renovate[bot] - format magic-string test before write to disk (#7945) by @IWANABETHATGUY - deps: update dependency rolldown-plugin-dts to ^0.21.0 (#7915) by @renovate[bot] - deps: update dependency oxlint-tsgolint to v0.11.1 (#7914) by @renovate[bot] - deps: update dependency diff to v8.0.3 [security] (#7904) by @renovate[bot] - remove outdated TODO comment in `collect_depended_symbols` (#7888) by @shulaoda - deps: update oxc resolver to v11.16.3 (#7876) by @renovate[bot]
Summary
This PR optimizes the symbol renamer by eliminating the expensive
rename_non_root_symbolpass and fixes a bug that could generate duplicate declarations.Background
This is a rework of #4014, split into atomic commits for easier review. The original PR was rejected due to too many changes at once. Issue #6586 has since been fixed by #7859, but the performance optimization approach from #4014 still provides significant gains.
Key Changes
1. Eliminate
rename_non_root_symbolpassis_name_available()register_nested_scope_symbols()2. Skip storing original-named nested symbols
canonical_name_for_or_original()method that falls back to original namecanonical_nameswhen actually renamed3. Fix duplicate declaration bug
a→a$1), check if the candidate name already exists in the same modulefunction test(a$1, a$1)when bothaanda$1exist as parametersHow It Works
Old algorithm:
New algorithm:
Test Case
Added
nested_scope_rename_countertest to verify the duplicate declaration fix:Before fix:
function test(a$1, a$1)- duplicate declarations!After fix:
function test(a$2, a$1)- correctly skipsa$1since it already existsSnapshot Changes
The optimization produces slightly different (but equally correct) variable names:
$1$1double-suffix patterns