Skip to content

Declare side-effecting modules so bundlers have better hints for what can be tree-shaken#21365

Open
NullVoxPopuli wants to merge 6 commits intomainfrom
nvp/configure-side-effects
Open

Declare side-effecting modules so bundlers have better hints for what can be tree-shaken#21365
NullVoxPopuli wants to merge 6 commits intomainfrom
nvp/configure-side-effects

Conversation

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

@NullVoxPopuli NullVoxPopuli commented May 3, 2026

Demo:


bundler hints that let consumer bundlers (vite/rolldown, webpack, esbuild) tree-shake aggressively through ember-source's internal barrel re-exports without requiring any source-file restructuring.

for the ember-source dist/tarball, the result is that chunks shake out a bit differently.

@NullVoxPopuli NullVoxPopuli marked this pull request as ready for review May 3, 2026 14:44
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

📊 Size report

Tarball size1.2 MB1.2 MB

dist/dev   -1.32%↓

File Before (Size / Brotli) After (Size / Brotli)
./packages/@glimmer/opcode-compiler/index.js 1.4 kB / 443 B -12.6%↓1.2 kB / -10.4%↓397 B
./packages/@glimmer/vm/index.js 565 B / 288 B 130%↑1.3 kB / 89%↑545 B
./packages/shared-chunks/assert-{hash}.js 619 B / 299 B -85.3%↓91 B / -72.2%↓83 B
./packages/shared-chunks/capabilities-{hash}.js 1.8 kB / 519 B -11%↓1.6 kB / -13.1%↓451 B
./packages/shared-chunks/curried-D5PiYPSB.js 163 B / 100 B
./packages/shared-chunks/fragment-Cc5k9Oy4.js 30.4 kB / 7.4 kB
./packages/shared-chunks/immediate-BFu4wr4m.js 1.1 kB / 355 B
./packages/shared-chunks/registers-ylirb0dq.js 973 B / 365 B
Total (Includes all files) 2.1 MB / 477.2 kB -1.32%↓2 MB / -1.45%↓470.3 kB

dist/prod   -1.45%↓

File Before (Size / Brotli) After (Size / Brotli)
./packages/@glimmer/opcode-compiler/index.js 1.3 kB / 416 B -10.6%↓1.1 kB / -11.5%↓368 B
./packages/@glimmer/vm/index.js 565 B / 288 B 130%↑1.3 kB / 89%↑545 B
./packages/shared-chunks/capabilities-{hash}.js 1.5 kB / 392 B -7.44%↓1.3 kB / -13.5%↓339 B
./packages/shared-chunks/curried-D5PiYPSB.js 163 B / 100 B
./packages/shared-chunks/fragment-D7nBU9ae.js 30.9 kB / 7.5 kB
./packages/shared-chunks/immediate-untnQeUA.js 1 kB / 350 B
./packages/shared-chunks/registers-ylirb0dq.js 973 B / 365 B
Total (Includes all files) 1.9 MB / 436.8 kB -1.45%↓1.8 MB / -1.54%↓430.1 kB

smoke-tests/v2-app-hello-world-template/dist   -31%↓

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 251.3 kB / 68.7 kB -31.1%↓173.2 kB / -30.6%↓47.6 kB
Total (Includes all files) 251.6 kB / 68.8 kB -31%↓173.6 kB / -30.6%↓47.8 kB

🤖 This report was automatically generated by wyvox/pkg-size

@NullVoxPopuli NullVoxPopuli marked this pull request as draft May 3, 2026 15:04
@NullVoxPopuli NullVoxPopuli changed the title Shrink hello-world bundle from 251 KB to 173 KB (-31%) via bundler hints Declare side-effecting modules so bundlers have better hints for what can be tree-shaken May 3, 2026
@NullVoxPopuli NullVoxPopuli marked this pull request as ready for review May 3, 2026 21:04
Comment thread bin/check-tree-shake.mjs
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

a manual lint, of sorts

NullVoxPopuli and others added 3 commits May 5, 2026 13:06
Two purely-additive bundler hints that let consumer bundlers
(vite/rolldown, webpack, esbuild) tree-shake aggressively through
ember-source's internal barrel re-exports without requiring any
source-file restructuring.

1. **`"sideEffects": false` on `ember-source/package.json`** -
   declares that no module in this package has top-level side
   effects that need to be preserved if the module's exports
   are unused. The bundler can then DCE re-exports through
   `index.ts` barrels that currently anchor the rest of the
   graph in place.

   This is safe in practice because rollup's chunking groups
   symbols with their side effects: any chunk containing the
   classic `Component` class also contains the
   `setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component)`
   call, the `setHelperManager` registrations live in the chunk
   that holds `helper.ts`, etc. Importing a symbol from a chunk
   pulls the chunk's side effects along; apps that don't reach
   those symbols don't need their side effects either.

2. **`treeshake.moduleSideEffects` callback in `rollup.config.mjs`** -
   the package-level `sideEffects: false` declarations on
   `@glimmer/debug`, `@glimmer/debug-util`, and
   `@glimmer/local-debug-flags` get lost when rolldown emits
   shared chunks (debug code from these packages can leak into
   chunks that the renderer-only path then pulls in). The callback
   re-asserts module purity at the chunk level so leaked debug
   code drops out of the renderer-only path.

Measured against `smoke-tests/v2-app-hello-world-template`:

| | raw | gzip |
| - | - | - |
| before | 251.05 KB | 79.75 KB |
| after  | 172.99 KB | 55.31 KB |

Classic `v2-app-template` and v1 `app-template` smoke tests still
build and pass. `pnpm test:node` 20/20. `pnpm vite build --mode
development` (full dev test suite app) builds clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`sideEffects: false` declares a contract that's invisible to
reviewers: any module-level mutation (`set*Manager(...)`,
`Foo.reopenClass(...)`, `_backburner = new Backburner(...)`)
silently rots the contract. Bundlers will drop these side effects
when the symbol they ride along with isn't imported.

Adds a `pnpm test:tree-shake` script that bundles each pure-today
entry point with rollup as if a consumer imported it for side
effects only, and asserts nothing survives. Regression in any
known-pure entry fails CI with an actionable message.

Today the list is 23 entries — `@glimmer/util`, `@glimmer/destroyable`,
`@ember/owner`, `@ember/version`, etc. The 43 currently-impure entries
(@ember/-internals/glimmer, @ember/runloop, @glimmer/runtime, …) are
omitted because they have known top-level side effects that
sideEffects: false promises bundlers can drop in practice anyway.

Patch: agadoo's bundled acorn pins ecmaVersion 11 (ES2020), which
chokes on private class fields and other ES2022+ syntax that appears
in dist. The patch bumps it to `'latest'`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Of the three entries previously in PURE_INTERNAL_PACKAGES, only
`@glimmer/debug` actually contributes to the hello-world bundle
size:

| array contents | hello-world raw | gzip |
| - | - | - |
| `[]` (no callback)                       | 181.12 KB | 57.82 KB |
| `['@glimmer/debug-util']`                 | 181.12 KB | 57.82 KB |
| `['@glimmer/local-debug-flags']`          | 181.12 KB | 57.82 KB |
| `['@glimmer/debug']`                      | 172.99 KB | 55.35 KB |
| all three                                 | 172.99 KB | 55.31 KB |

`@glimmer/debug-util` and `@glimmer/local-debug-flags` are
already inferred pure by rolldown's static analysis (their content
either has no top-level statements that look like side effects, or
gets fully stripped by the LOCAL_DEBUG macro in prod builds).
Re-asserting them adds nothing.

Inlined the array since the callback now checks a single path
prefix. Added a note explaining the measurement so the next
person adding to the list does so with data, not pattern-matching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NullVoxPopuli NullVoxPopuli force-pushed the nvp/configure-side-effects branch from fcead41 to b5304e9 Compare May 5, 2026 17:07
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.

1 participant