Skip to content

fix: handle cjs export assignment side effects#14260

Open
JSerFeng wants to merge 1 commit into
mainfrom
fy/cjs-export-assignment-side-effects
Open

fix: handle cjs export assignment side effects#14260
JSerFeng wants to merge 1 commit into
mainfrom
fy/cjs-export-assignment-side-effects

Conversation

@JSerFeng

@JSerFeng JSerFeng commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Treat static CommonJS export assignments as side-effect-free with respect to the assignment target and evaluate the RHS initializer instead.
  • Mark CommonJS export dependencies and only assignment-target CommonJS self-reference dependencies as evaluation side-effect-free, so real CommonJS reads/calls still keep modules alive.
  • Add a tree-shaking fixture covering pure module.exports / exports.foo assignments and an impure RHS assignment that must remain.

Root cause

CommonJS export assignments such as exports.foo = value and module.exports = value were conservatively treated as side-effectful because the assignment/member access participated in module evaluation side-effect checks. For module.exports = value, walking the assignment target can also create a self-reference dependency for the left-hand module.exports; that dependency must be evaluation side-effect-free only in assignment-target position, otherwise normal CommonJS self references can be incorrectly dropped.

Validation

  • cargo fmt --package rspack_plugin_javascript
  • pnpm run build:binding:dev
  • pnpm --dir tests/rspack-test run test -- TreeShaking.test.js -t cjs-export-assignment-side-effects
  • pnpm --dir tests/rspack-test run test -- Config.part2.test.js -t "entry/depend-on-bug|library/modern-module-force-concaten"
  • pnpm --dir tests/rspack-test run test -- StatsOutput.test.js -t side-effects-optimization

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

📦 Binary Size-limit

Comparing ccaf6c4 to feat: support rspack magic comment prefix (#14323) by AsyncIter

❌ Size increased by 4.00KB from 62.60MB to 62.60MB (⬆️0.01%)

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Rsdoctor Bundle Diff Analysis

Found 5 projects in monorepo, 1 project with changes.

📊 Quick Summary
Project Total Size Gzip Size Change Gzip Change
popular-libs 1.7 MB 551.3 KB 0 0
react-10k 5.6 MB 1.3 MB 0 0
ui-components 4.8 MB 1.4 MB -880.0 B (-0.0%) -511.0 B (-0.0%)
react-1k 822.8 KB 218.3 KB 0 0
react-5k 2.7 MB 669.1 KB 0 0
📋 Detailed Reports (Click to expand)

📁 ui-components

Path: ../build-tools-performance/cases/ui-components/dist/rsdoctor-data.json

📌 Baseline Commit: b04d9f41ab | PR: #14323

Metric Current Baseline Change
📊 Total Size 4.8 MB 4.8 MB -880.0 B (-0.0%)
🗜️ Gzip Size 1.4 MB 1.4 MB -511.0 B (-0.0%)
📄 JavaScript 4.7 MB 4.7 MB -880.0 B (-0.0%)
🎨 CSS 111.7 KB 111.7 KB 0
🌐 HTML 328.0 B 328.0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: ui-components Bundle Diff

🤖 AI Degradation Analysis (Click to expand)

📊 Size Changes

Asset / Chunk Baseline Current Δ Size Δ % Initial?
No significant regressions detected 🎉

Win: Total bundle size decreased by 880 bytes (5,079,179 → 5,078,299).

🔍 Root Cause Analysis

  • Minor fluctuation in is-mobile@5.0.0 (gzip +99 bytes), likely due to build hash or content changes.
  • Overall JS initial chunk decreased by 880 bytes (4,964,432 → 4,963,552), offsetting module-level increases.
  • No new dependencies added or removed.

⚠️ Risk Assessment

Overall severity: Low

  • Total bundle size decreased; initial chunk impact is negligible and positive for load time.

💡 Optimization Suggestions

  1. No action required: Current build configuration is effective.
  2. Monitor: Keep an eye on is-mobile in future updates if gzip size continues to trend up despite tree-shaking.
  3. Maintain: Ensure tree-shaking settings remain aggressive to keep parsed sizes minimal.

Analysis by qwen3.5-plus

Generated by Rsdoctor GitHub Action

@codspeed-hq

codspeed-hq Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 23.03%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

❌ 1 regressed benchmark
✅ 51 untouched benchmarks
⏩ 40 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime bundle@threejs-10x-development 237.1 ms 308 ms -23.03%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fy/cjs-export-assignment-side-effects (ccaf6c4) with main (b04d9f4)

Open in CodSpeed

Footnotes

  1. 40 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@JSerFeng JSerFeng force-pushed the fy/cjs-export-assignment-side-effects branch 3 times, most recently from 7f086af to 1b48b04 Compare June 5, 2026 07:51
@JSerFeng JSerFeng marked this pull request as ready for review June 8, 2026 02:39
Copilot AI review requested due to automatic review settings June 8, 2026 02:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b48b0460d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

unresolved_ctxt,
comments,
),
Expr::Assign(assign_expr) if is_common_js_export_assignment(assign_expr) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit CommonJS export purity to CommonJS modules

This branch runs in the generic side-effects analyzer without checking parser.is_esm, so an ESM file such as export {}; exports.foo = 1; imported only for side effects is now classified as side-effect-free when the RHS is pure. In ESM that assignment is not a CommonJS export write: it can mutate a global exports object or throw a ReferenceError, both of which are observable, so the optimizer can incorrectly drop the module.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves tree-shaking accuracy for CommonJS modules by treating static export assignments (e.g. exports.foo = ..., module.exports = ...) as evaluation-side-effect-free for the assignment target, while still preserving RHS initializer side effects and preserving true CommonJS reads/calls that should keep modules alive.

Changes:

  • Adjust side-effects analysis to consider CommonJS export assignments pure w.r.t. the LHS target and only evaluate/preserve RHS side effects.
  • Track “assignment-target context” during AST walking and propagate it into CommonJS self-reference dependencies to avoid keeping modules alive due to LHS self-references.
  • Add a dedicated tree-shaking fixture + snapshots covering pure export assignments vs. an impure RHS that must remain.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/rspack.config.js Adds a tree-shaking test config enabling sideEffects optimization.
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/pure-named-exports.js Fixture: pure exports.* assignments intended to be droppable.
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/pure-module-exports.js Fixture: pure module.exports = ... assignment intended to be droppable.
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/impure-named-exports.js Fixture: RHS side effect that must be preserved.
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/index.js Entry importing fixtures to validate elimination/preservation behavior.
tests/rspack-test/treeShakingCases/cjs-export-assignment-side-effects/snapshots/treeshaking.snap.txt Snapshot asserting only RHS side effects remain and pure export assignments are dropped.
tests/rspack-test/statsOutputCases/side-effects-optimization/snapshots/stats.txt Updates stats snapshot to reflect improved side-effects detection output.
crates/rspack_plugin_javascript/src/visitors/dependency/parser/mod.rs Adds in_assign_target parser state flag.
crates/rspack_plugin_javascript/src/visitors/dependency/parser/walk.rs Sets/restores in_assign_target while walking assignment targets.
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs Treats CommonJS export assignments as pure w.r.t. LHS and evaluates RHS for side effects.
crates/rspack_plugin_javascript/src/parser_plugin/common_js_exports_parse_plugin.rs Threads in_assign_target into CommonJsSelfReferenceDependency creation.
crates/rspack_plugin_javascript/src/dependency/commonjs/common_js_self_reference_dependency.rs Adds evaluation_side_effect_free and implements evaluation side-effects state accordingly.
crates/rspack_plugin_javascript/src/dependency/commonjs/common_js_exports_dependency.rs Marks CommonJS export dependencies as evaluation side-effect-free.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

true
}

fn is_common_js_export_assignment(expr: &AssignExpr) -> bool {
Comment on lines +1592 to +1596
match current_member.obj.as_ref() {
Expr::Ident(ident) => {
members.push(ident.sym.clone());
break;
}
Comment on lines +1647 to +1648
Expr::Assign(assign_expr) if is_common_js_export_assignment(assign_expr) => {
is_pure_expression(
@JSerFeng JSerFeng force-pushed the fy/cjs-export-assignment-side-effects branch from 1b48b04 to 61c0669 Compare June 8, 2026 03:46

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61c066974d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

parser.module_has_side_effects = true;
}

if !has_side_effects && parser.side_effects_item.is_none() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve callee checks after pure CJS exports

When experiments.pureFunctions is enabled, identifier calls such as doWork() are first treated as pure and only become a bailout/deferred check in the callees loop below; however a preceding pure CJS export assignment now sets parser.side_effects_item while leaving module_has_side_effects false, so this guard skips that loop for subsequent statements. A module like exports.foo = 1; doWork(); can therefore stay side_effect_free and be dropped when imported only for side effects, losing doWork()'s effects.

Useful? React with 👍 / 👎.

@JSerFeng JSerFeng force-pushed the fy/cjs-export-assignment-side-effects branch from 61c0669 to 8c4262c Compare June 9, 2026 08:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c4262c2b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


if compiler_options.optimization.side_effects.is_true() {
let has_side_effects = side_effects_item.is_some();
let has_side_effects = module_has_side_effects;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve ESM export declaration side effects

Using only module_has_side_effects here drops side effects that are reported from the module_declaration hook: that hook still sets only parser.side_effects_item for impure export default / export const initializers and never sets the new flag. With optimization.sideEffects, a module such as export default console.log("keep") can therefore be marked side_effect_free and be removed when imported only for side effects, even though the export initializer runs during module evaluation.

Useful? React with 👍 / 👎.

Comment on lines +1655 to +1656
matches!(members.as_slice(), [first, ..] if first == "exports" && members.len() > 1)
|| matches!(members.as_slice(), [first, second, ..] if first == "module" && second == "exports")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve reads in deep CommonJS export assignments

This treats every chain beginning with exports or module.exports as a pure export target, but deep writes like exports.foo.bar = 1 and module.exports.foo.bar = 1 first read exports.foo / module.exports.foo; when that property is missing (or has a getter), the read can throw or run user code. If such a module is imported only for side effects, it can now be classified as side-effect-free and removed, hiding that observable read/throw. The special case should be limited to direct export writes unless the intermediate object access is also proven safe.

Useful? React with 👍 / 👎.

@JSerFeng JSerFeng force-pushed the fy/cjs-export-assignment-side-effects branch 3 times, most recently from 78a265f to 7bde384 Compare June 9, 2026 15:56
@JSerFeng JSerFeng force-pushed the fy/cjs-export-assignment-side-effects branch from 7bde384 to ccaf6c4 Compare June 9, 2026 16:10
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.

2 participants