Skip to content

feat(tree-shaking): recognize pure global constructors and calls#13701

Open
JSerFeng wants to merge 1 commit intomainfrom
fy/vigorous-wilbur
Open

feat(tree-shaking): recognize pure global constructors and calls#13701
JSerFeng wants to merge 1 commit intomainfrom
fy/vigorous-wilbur

Conversation

@JSerFeng
Copy link
Copy Markdown
Contributor

Summary

  • Teach Rspack's side-effect detector about built-in pure globals so unused new Set(), new Map(), new Uint8Array(), Object.keys(x), Array.isArray(x), String(x), etc. are now tree-shaken.
  • Previously, SWC's may_have_side_effects only recognized Date as a pure callee and only empty-fn / pure-class expressions as pure new callees — so export const cache = new Map() was always retained even when cache was unused. Rolldown handles this via oxc_ecmascript::side_effects::MayHaveSideEffects, which maintains a pure-globals list; this PR mirrors that approach in Rspack.
  • Shadowing-safe: the callee must resolve to an unresolved global (ctxt == unresolved_ctxt), so const Set = sideEffect(); new Set() is still kept. Arguments are recursively checked via the existing are_pure_args / is_pure_call_args helpers, so new Set([impureArg()]) is still kept.

Lists

  • new pure constructors: Set, Map, WeakSet, WeakMap, Object, Array, String, Number, Boolean, Date, ArrayBuffer, SharedArrayBuffer, and all 11 TypedArrays (Uint8Array, Int8Array, Uint8ClampedArray, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array).
  • Direct-call pure globals: Array, Object, String, Number, Boolean, Symbol, Date.
  • Member-call pure globals: Object.{keys,values,entries,getOwnPropertyNames,getOwnPropertySymbols,getOwnPropertyDescriptor,getOwnPropertyDescriptors,getPrototypeOf,create,freeze,fromEntries,is,assign}, Array.{isArray,from,of}.
  • Conservatively omitted (conditional on args): BigInt, RegExp, Error/TypeError/…, Promise, Reflect.*, JSON.*. These can be added later with argument validation.

Test plan

  • cargo build -p rspack_plugin_javascript
  • cargo test -p rspack_plugin_javascript --lib side_effects
  • CI: new treeshaking case at tests/rspack-test/treeShakingCases/global-pure-new/ (snapshot will be generated by CI — includes happy path, shadowed-binding guard, and impure-arg guard).
  • CI: verify existing tests/rspack-test/treeShakingCases/pure_comments_new_expr/ still passes (unchanged behavior: inner new Set() inside a user function is still tied to the function's lifetime).

Teach the side-effect detector about built-in pure globals so that
unused `new Set()`, `new Map()`, `new Uint8Array()`, `Object.keys(x)`,
`Array.isArray(x)`, `String(x)`, etc. can be tree-shaken. SWC's
`may_have_side_effects` only recognized `Date` and empty fn/class
callees; everything else was kept unnecessarily.

The callee must resolve to an unresolved global (ctxt check) so
shadowed bindings like `const Set = ...; new Set()` are preserved.
Arguments are still recursively checked for purity via the existing
`are_pure_args` / `is_pure_call_args` helpers.
Copilot AI review requested due to automatic review settings April 14, 2026 07:36
@github-actions github-actions bot added team The issue/pr is created by the member of Rspack. release: feature release: feature related release(mr only) labels Apr 14, 2026
Copy link
Copy Markdown

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

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: f0ce1c562a

ℹ️ 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".

Comment on lines +1165 to +1168
| "freeze"
| "fromEntries"
| "is"
| "assign"
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 Remove Object.assign/freeze from pure member-call allowlist

Object.assign and Object.freeze mutate the object passed in, so they are observable even when the return value is unused. Because is_pure_member_call now classifies both as pure and is_pure_call_args only checks argument expression purity, tree-shaking can incorrectly drop calls like Object.assign(config, defaults) or Object.freeze(exportsObj), changing later reads/exports of that object.

Useful? React with 👍 / 👎.

| "is"
| "assign"
),
"Array" => matches!(prop.sym.as_str(), "isArray" | "from" | "of"),
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 Treat Array.from mapper invocations as side effects

Array.from is not side-effect free by default: it executes the iterable protocol and may call the optional mapper function for each element. Marking it as pure here lets DCE remove statements such as Array.from(items, log) when the result is unused, which drops the mapper/iterator side effects and changes runtime behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 expands Rspack’s side-effect analysis to treat certain built-in global constructors and global/member calls as pure (when their arguments are considered pure), enabling more aggressive tree-shaking of unused expressions like new Map() and Object.keys(x). It also adds a new tree-shaking fixture to validate the intended behavior.

Changes:

  • Add a “pure globals” fast-path for call expressions (direct globals and selected Object.* / Array.* member calls).
  • Add a “pure globals” fast-path for new expressions with selected built-in constructors.
  • Introduce a new tree-shaking test case (global-pure-new) to validate unused pure constructions/calls are eliminated while keeping shadowed/impure cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
crates/rspack_plugin_javascript/src/parser_plugin/side_effects_parser_plugin.rs Adds allowlists and fast-path checks for treating certain global calls/constructors as pure.
tests/rspack-test/treeShakingCases/global-pure-new/app.js Adds a fixture module meant to test tree-shaking of pure globals + guards.
tests/rspack-test/treeShakingCases/global-pure-new/index.js Imports/uses an exported marker to ensure the module is included in the bundle.
tests/rspack-test/treeShakingCases/global-pure-new/rspack.config.js Enables optimization.sideEffects for the new tree-shaking fixture.

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

| "is"
| "assign"
),
"Array" => matches!(prop.sym.as_str(), "isArray" | "from" | "of"),
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Including Array.from in the pure member-call allowlist is not safe: Array.from(iterable, mapFn) can invoke user-provided iteration and the optional mapping function, producing side effects even when the argument expressions themselves are “pure” (e.g. a pure arrow/function expression). Consider removing from from the allowlist or only treating it as pure for the 1-arg form when the input is a trivially safe literal (and no mapFn/thisArg).

Suggested change
"Array" => matches!(prop.sym.as_str(), "isArray" | "from" | "of"),
"Array" => matches!(prop.sym.as_str(), "isArray" | "of"),

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
const ShadowedSet = sideEffect();
let shadowed = new ShadowedSet();
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The “Shadowed — must NOT be treated as pure” test doesn’t actually shadow a built-in global: ShadowedSet is a new identifier and will never match the Set/Map allowlist, so this doesn’t exercise the ctxt == unresolved_ctxt shadowing guard. Consider shadowing the real name instead (e.g. const Set = sideEffect(); new Set()) so this case fails if the unresolved-global check is broken.

Suggested change
const ShadowedSet = sideEffect();
let shadowed = new ShadowedSet();
const Set = sideEffect();
let shadowed = new Set();

Copilot uses AI. Check for mistakes.
Comment on lines +1085 to +1089
// Fast path: known pure global constructors (`new Set()`, `new Map()`,
// `new WeakMap()`, TypedArrays, etc.). SWC's `is_pure_new_callee` only
// recognizes empty functions and pure class expressions, so without this
// fast path `let m = new Map()` is kept even when unused.
if let Expr::Ident(ident) = new_expr.callee.as_ref()
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

is_pure_new_global treats constructors like Set/Map/TypedArrays as pure as long as the argument expressions are pure, but these constructors can execute user code based on argument values (e.g. new Set(iterable) consumes iterators; typed array/buffer constructors coerce via valueOf/toString). This can cause tree-shaking to drop observable side effects. Consider restricting the fast-path to cases that are semantically side-effect-free (e.g. only zero-arg new Set()/new Map(), and for typed arrays only a numeric length literal), or add argument-shape/value-coercion guards before returning pure.

Copilot uses AI. Check for mistakes.
Comment on lines +1129 to +1134
/// Functions that are pure when called directly, assuming args are pure.
fn is_pure_call_global(name: &str) -> bool {
matches!(
name,
"Array" | "Object" | "String" | "Number" | "Boolean" | "Symbol" | "Date"
)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Treating direct calls like String(x), Number(x), Symbol(x), and Date(x) as “pure when args are pure” is not semantically safe: these built-ins can invoke user code via @@toPrimitive / valueOf / toString when x is an object, even if the argument expression itself is “pure” (e.g. an object literal). To avoid dropping side effects, consider limiting this fast-path to 0-arg calls or to arguments that are guaranteed primitives (literals), or add a conservative check that rejects object/nullable arguments for these callees.

Suggested change
/// Functions that are pure when called directly, assuming args are pure.
fn is_pure_call_global(name: &str) -> bool {
matches!(
name,
"Array" | "Object" | "String" | "Number" | "Boolean" | "Symbol" | "Date"
)
/// Functions that are pure when called directly, assuming args are pure,
/// and without requiring argument coercions that can invoke user code.
fn is_pure_call_global(name: &str) -> bool {
matches!(name, "Array" | "Object" | "Boolean")

Copilot uses AI. Check for mistakes.
Comment on lines +1164 to +1168
| "create"
| "freeze"
| "fromEntries"
| "is"
| "assign"
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The member-call allowlist includes methods like Object.assign, Object.create, and Object.fromEntries which can execute user code (getters/setters during property reads/writes, iterator consumption, Proxy traps) even when the argument expressions are “pure”. Treating these as side-effect-free risks incorrect elimination. Consider removing the mutating/iterating members from the allowlist (e.g. at least assign/fromEntries/create), or adding strict argument-shape validation so only trivially safe cases are considered pure.

Suggested change
| "create"
| "freeze"
| "fromEntries"
| "is"
| "assign"
| "freeze"
| "is"

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

Rsdoctor Bundle Diff Analysis

⚠️ Note: The latest commit (817208df9c) does not have baseline artifacts. Using commit edb82110da for baseline comparison instead. If this seems incorrect, please wait a few minutes and try rerunning the workflow.

Found 6 projects in monorepo, 6 projects with changes.

📊 Quick Summary
Project Total Size Change
popular-libs 1.7 MB -152.0 B (-0.0%)
react-10k 5.7 MB -81.0 B (-0.0%)
react-1k 826.1 KB -81.0 B (-0.0%)
rome 984.1 KB -9.0 B (-0.0%)
react-5k 2.7 MB -81.0 B (-0.0%)
ui-components 4.9 MB -9.7 KB (-0.2%)
📋 Detailed Reports (Click to expand)

📁 popular-libs

Path: ../build-tools-performance/cases/popular-libs/dist/rsdoctor-data.json

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 1.7 MB 1.7 MB -152.0 B (-0.0%)
📄 JavaScript 1.7 MB 1.7 MB -152.0 B (-0.0%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: popular-libs Bundle Diff

📁 react-10k

Path: ../build-tools-performance/cases/react-10k/dist/rsdoctor-data.json

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 5.7 MB 5.7 MB -81.0 B (-0.0%)
📄 JavaScript 5.7 MB 5.7 MB -81.0 B (-0.0%)
🎨 CSS 21.0 B 21.0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: react-10k Bundle Diff

📁 react-1k

Path: ../build-tools-performance/cases/react-1k/dist/rsdoctor-data.json

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 826.1 KB 826.2 KB -81.0 B (-0.0%)
📄 JavaScript 826.1 KB 826.2 KB -81.0 B (-0.0%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: react-1k Bundle Diff

📁 rome

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

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 984.1 KB 984.1 KB -9.0 B (-0.0%)
📄 JavaScript 984.1 KB 984.1 KB -9.0 B (-0.0%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: rome Bundle Diff

📁 react-5k

Path: ../build-tools-performance/cases/react-5k/dist/rsdoctor-data.json

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 2.7 MB 2.7 MB -81.0 B (-0.0%)
📄 JavaScript 2.7 MB 2.7 MB -81.0 B (-0.0%)
🎨 CSS 21.0 B 21.0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: react-5k Bundle Diff

📁 ui-components

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

📌 Baseline Commit: edb82110da | PR: #13699

Metric Current Baseline Change
📊 Total Size 4.9 MB 5.0 MB -9.7 KB (-0.2%)
📄 JavaScript 4.7 MB 4.7 MB -9.7 KB (-0.2%)
🎨 CSS 291.6 KB 291.6 KB 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: ui-components Bundle Diff

Generated by Rsdoctor GitHub Action

@github-actions
Copy link
Copy Markdown
Contributor

📦 Binary Size-limit

Comparing f0ce1c5 to fix: Revert rstest importFunctionName feature when importDynamic is disabled (#13699) by 9aoy

🎉 Size decreased by 3.47KB from 49.39MB to 49.38MB (⬇️0.01%)

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 14, 2026

Merging this PR will improve performance by 2.97%

⚡ 2 improved benchmarks
✅ 26 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation rust@create_chunk_ids 10.6 ms 10.2 ms +2.97%
Simulation rust@persistent_cache_restore@basic-react-development 26.6 ms 26.1 ms +2.05%

Comparing fy/vigorous-wilbur (f0ce1c5) with main (edb8211)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (817208d) during the generation of this report, so edb8211 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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

Labels

release: feature release: feature related release(mr only) team The issue/pr is created by the member of Rspack.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants