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
Closed
Conversation
Contributor
📊 Size reportTarball size — dist/dev -1.52%↓
dist/prod -1.45%↓
smoke-tests/v2-app-hello-world-template/dist -31.1%↓
🤖 This report was automatically generated by wyvox/pkg-size |
4ca87fb to
90e0498
Compare
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>
90e0498 to
1dc6a36
Compare
cdfeb88 to
4de95e9
Compare
`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>
4de95e9 to
e8bb3bb
Compare
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Measured against
smoke-tests/v2-app-hello-world-template. Classicv2-app-templateand v1app-templatesmoke tests still build and pass.Changes
1.
"sideEffects": falseonember-source/package.jsonDeclares 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.tsbarrels 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:
Componentclass also contains thesetInternalComponentManager(CURLY_COMPONENT_MANAGER, Component)call.helper.tsexports also contains its top-levelsetHelperManager(...)registrations.setupApplicationRegistryalso containsenvironment.ts'ssetGlobalContext({...})call.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
sideEffectsarray listing the relevant chunk patterns.2.
treeshake.moduleSideEffectscallback inrollup.config.mjsThe package-level
sideEffects: falsedeclarations on@glimmer/debug,@glimmer/debug-util, and@glimmer/local-debug-flagsget 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: falseis 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.tsfiles: a barrel that doesexport { foo } from './foo'; export { bar } from './bar'no longer has to load./barwhen onlyfoois used.Relationship to other PRs
maindirectly, landing at ~174 KB / ~56 KB gzip (-30%).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-templatebuilds (251 → 173 KB / 80 → 55 KB gzip)smoke-tests/v2-app-template(classic v2 app) builds + 1/1 test passessmoke-tests/app-template(v1 app) builds + 1/1 test passespnpm test:node20/20pnpm vite build --mode development --minify false(full dev test suite app) builds clean🤖 Generated with Claude Code