Skip to content

Shrink hello-world bundle from 251 KB to 173 KB (-31%) via bundler hints#21362

Closed
NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:claude/shrink-hello-world-config-only
Closed

Shrink hello-world bundle from 251 KB to 173 KB (-31%) via bundler hints#21362
NullVoxPopuli-ai-agent wants to merge 2 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:claude/shrink-hello-world-config-only

Conversation

@NullVoxPopuli-ai-agent
Copy link
Copy Markdown
Contributor

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented May 3, 2026

Two purely-additive bundler hints — no source-file restructuring — that let consumer bundlers (vite/rolldown, webpack, esbuild) tree-shake aggressively through ember-source's internal barrel re-exports.

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

Measured against smoke-tests/v2-app-hello-world-template. Classic v2-app-template and v1 app-template smoke tests still build and pass.

Changes

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. Bundlers 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:

  • The chunk containing the classic Component class also contains the setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component) call.
  • The chunk containing helper.ts exports also contains its top-level setHelperManager(...) registrations.
  • The chunk containing setupApplicationRegistry also contains environment.ts's setGlobalContext({...}) call.
  • Opcode handlers, runloop init, validator init — same pattern.

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.

It's a contract: anyone refactoring ember-source's internals later must ensure side-effect-only modules remain reachable through an exported symbol, or explicitly switch to a sideEffects array listing the relevant chunk patterns.

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.

Why this works without source changes

Bundlers default to assuming any imported file might have load-bearing top-level side effects, so they preserve every imported file conservatively. sideEffects: false is a contract telling the bundler "you can trust me — none of my files have side effects worth preserving on their own."

That unlocks aggressive tree-shaking through barrel index.ts files: a barrel that does export { foo } from './foo'; export { bar } from './bar' no longer has to load ./bar when only foo is used.

Relationship to other PRs

These three PRs stack cleanly: bundler hints (this PR) → structural refactor (#21360 / #21359) → barrel-import cleanup (#21350).

Test plan

  • smoke-tests/v2-app-hello-world-template builds (251 → 173 KB / 80 → 55 KB gzip)
  • smoke-tests/v2-app-template (classic v2 app) builds + 1/1 test passes
  • smoke-tests/app-template (v1 app) builds + 1/1 test passes
  • pnpm test:node 20/20
  • pnpm vite build --mode development --minify false (full dev test suite app) builds clean
  • Browser tests pass in CI

🤖 Generated with Claude Code

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

NullVoxPopuli commented May 3, 2026

📊 Size report

Tarball size1.2 MB1.2 MB

dist/dev   -1.52%↓

File Before (Size / Brotli) After (Size / Brotli)
./packages/@glimmer/destroyable/index.js 6.4 kB / 1.5 kB -94.6%↓346 B / -89.1%↓160 B
./packages/@glimmer/opcode-compiler/index.js 1.4 kB / 443 B -16.8%↓1.1 kB / -17.8%↓364 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/compiler-{hash}.js 178.8 kB / 34.2 kB -0.07%↓178.7 kB / -0.11%↓34.2 kB
./packages/shared-chunks/curried-D5PiYPSB.js 163 B / 100 B
./packages/shared-chunks/debug-to-string-CFb7h0lY.js 1.7 kB / 585 B
./packages/shared-chunks/fragment-Cc5k9Oy4.js 30.4 kB / 7.4 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) 2 MB / 476.3 kB -1.52%↓2 MB / -1.66%↓468.4 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 / -10.1%↓374 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 kB -1.45%↓1.8 MB / -1.52%↓429.4 kB

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

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 251.1 kB / 68.6 kB -31.1%↓173 kB / -30.7%↓47.6 kB
Total (Includes all files) 251.4 kB / 68.8 kB -31.1%↓173.3 kB / -30.6%↓47.7 kB

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

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent force-pushed the claude/shrink-hello-world-config-only branch from 4ca87fb to 90e0498 Compare May 3, 2026 05:13
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent changed the title Shrink hello-world bundle from 251 KB to 181 KB (-28%) via bundler hints Shrink hello-world bundle from 251 KB to 173 KB (-31%) via bundler hints May 3, 2026
@NullVoxPopuli NullVoxPopuli marked this pull request as draft May 3, 2026 05:17
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>
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent force-pushed the claude/shrink-hello-world-config-only branch from 90e0498 to 1dc6a36 Compare May 3, 2026 05:17
@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent force-pushed the claude/shrink-hello-world-config-only branch 3 times, most recently from cdfeb88 to 4de95e9 Compare May 3, 2026 15:36
`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>
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