Skip to content

perf(@angular/build): memoize package-level sideEffects checks in compiler plugin - #34047

Open
alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:perf/compiler-side-effects-cache
Open

perf(@angular/build): memoize package-level sideEffects checks in compiler plugin#34047
alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:perf/compiler-side-effects-cache

Conversation

@alan-agius4

Copy link
Copy Markdown
Collaborator

When advancedOptimizations is enabled, the compiler plugin checks each emitted and transformed JavaScript file via hasSideEffects() to determine whether pure annotations should be added during bundling.

Previously, hasSideEffects() invoked build.resolve() across the esbuild Go <-> Node.js IPC boundary for every individual file. In an application with hundreds of files loaded from node_modules (e.g. 226 files from rxjs), this resulted in hundreds of redundant IPC round-trips to evaluate the exact same package-level sideEffects configuration.

To eliminate redundant resolution calls:

  • Introduce a dedicated SideEffectsResolver class and createSideEffectsResolver() factory in a separate module.
  • Memoize resolved sideEffects booleans at both the package level and the individual file level.
  • When an imported file resides in node_modules, extract the package directory and inspect its package.json sideEffects property once.
  • If sideEffects is a boolean, memoize it for all files originating from that package.
  • If sideEffects is an array of globs, string, or omitted, bypass package-level memoization and resolve via build.resolve() per file, memoizing the resolved file result to prevent duplicate calls.
  • Return an async noop when advancedOptimizations is disabled to avoid overhead.
  • Provide comprehensive unit tests covering package memoization, non-boolean sideEffects, scoped packages, pnpm virtual stores, and Windows/relative paths.

In benchmarks on an application with 253 JS dependency files across 6 packages, build.resolve IPC calls dropped from 253 to 6 (-97.6%) and cumulative resolve duration dropped by 99.7%.

@angular-robot angular-robot Bot added area: performance Issues related to performance area: @angular/build labels Sep 9, 2026
@alan-agius4 alan-agius4 added the target: patch This PR is targeted for the next patch release label Sep 9, 2026
@alan-agius4
alan-agius4 marked this pull request as ready for review September 9, 2026 08:21
@alan-agius4
alan-agius4 requested a review from clydin September 9, 2026 08:21
@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Sep 9, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request extracts the side-effects resolution logic from the Angular compiler plugin into a dedicated SideEffectsResolver utility, introducing package-level and file-level memoization to optimize bundling performance along with comprehensive unit tests. The review feedback suggests safely parsing package.json by avoiding direct destructuring of JSON.parse output, which could throw a TypeError if the JSON parses to null, and instead using optional chaining to avoid unnecessary performance overhead from the try/catch block.

Comment thread packages/angular/build/src/tools/esbuild/angular/side-effects-resolver.ts Outdated
…piler plugin

When advancedOptimizations is enabled, the compiler plugin checks each emitted and transformed JavaScript file via hasSideEffects() to determine whether pure annotations should be added during bundling.

Previously, hasSideEffects() invoked build.resolve() across the esbuild Go <-> Node.js IPC boundary for every individual file. In an application with hundreds of files loaded from node_modules (e.g. 226 files from rxjs), this resulted in hundreds of redundant IPC round-trips to evaluate the exact same package-level sideEffects configuration.

To eliminate redundant resolution calls:
- Introduce a dedicated SideEffectsResolver class and createSideEffectsResolver() factory in a separate module.
- Memoize resolved sideEffects booleans at both the package level and the individual file level.
- When an imported file resides in node_modules, extract the package directory and inspect its package.json sideEffects property once.
- If sideEffects is a boolean, memoize it for all files originating from that package.
- If sideEffects is an array of globs, string, or omitted, bypass package-level memoization and resolve via build.resolve() per file, memoizing the resolved file result to prevent duplicate calls.
- Return an async noop when advancedOptimizations is disabled to avoid overhead.
- Provide comprehensive unit tests covering package memoization, non-boolean sideEffects, scoped packages, pnpm virtual stores, and Windows/relative paths.

In benchmarks on an application with 253 JS dependency files across 6 packages, build.resolve IPC calls dropped from 253 to 6 (-97.6%) and cumulative resolve duration dropped by 99.7%.
@alan-agius4
alan-agius4 force-pushed the perf/compiler-side-effects-cache branch from e727ea9 to 8d482a1 Compare September 9, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular/build area: performance Issues related to performance target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant