Shrink hello-world bundle from 251 KB to 174 KB (-30%)#21360
Draft
NullVoxPopuli-ai-agent wants to merge 2 commits intoemberjs:mainfrom
Draft
Shrink hello-world bundle from 251 KB to 174 KB (-30%)#21360NullVoxPopuli-ai-agent wants to merge 2 commits intoemberjs:mainfrom
NullVoxPopuli-ai-agent wants to merge 2 commits intoemberjs:mainfrom
Conversation
Adds `"./*": "./*.ts"` to the `exports` map of every internal @ember and @Glimmer package. This enables deep imports such as `@ember/-internals/metal/lib/property_set` to resolve against the source file directly. Required by the follow-up commit, which uses deep imports to break specific dependency chains that the renderer-only path should not pay for. With only the barrel exports available, those imports would need to go through `index.ts`, dragging in the full barrel transitively. Ported from emberjs#21350 (the package.json subset only; no source-file changes from that PR are included). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cuts the hello-world smoke test (`smoke-tests/v2-app-hello-world-template`) from 251.05 KB / 79.75 KB gzip -> 174.28 KB / 55.76 KB gzip while leaving classic `v2-app-template` builds + tests passing. The win comes from breaking specific dependency chains that pulled the classic-Ember-object stack (Mixin, Observable, Evented, classic Component, computed, observers, RSVP, routing) into the renderer-only path even when an app uses only `@glimmer/component` + `@tracked`. ## Pattern Three shapes recur: 1. **Side-effect-only registration module** that classic-app `setup-registry` imports - bundles that don't pull in `setup-registry` (the `renderComponent`-only path) skip the registration and the heavy module it pulls in. 2. **Registration hook on the consumer side** so the consumer module no longer statically imports the producer; the producer registers itself as a top-level side effect when loaded by anything else. 3. **Hot-path utility moved to its own file** so deep imports skip the heavy module's other side effects. Wrapped up with a `sideEffects` field on `ember-source/package.json` listing the actual side-effect files, so bundlers can tree-shake the rest of the graph aggressively. ## Changes 1. **Lazy `-mount` and `-outlet` keyword registration** - `resolver.ts` no longer statically imports `mountHelper`/ `outletHelper`. Replaced with `registerBuiltInKeywordHelper(name, helper)` and a side-effect file `syntax/register-routing-keywords.ts` imported by `setup-registry.ts`. Drops ~138 KB of `@ember/routing` + ~7 KB of `@ember/engine` from the renderer-only path. 2. **Split classic `Renderer` subclass into `classic-renderer.ts`** - moved `Renderer extends BaseRenderer`, `ClassicRootState`, the concrete `DynamicScope` class, and the `View` interface out of `renderer.ts`. Added a `RootState` interface so `RendererState` is generic over either root type. 3. **`RSVP.defer` -> native `Promise` in `renderSettled`** - together with #1, lets the bundle drop the 62 KB rsvp shared chunk entirely. 4. **Curly symbols extracted to `curly-symbols.ts`** - `isCurlyManager` is now a brand check (`manager[CURLY_COMPONENT_BRAND] === true`) instead of an instance check, so the resolver no longer pulls `component-managers/curly.ts` (the full `CurlyComponentManager` lifecycle, ~17 KB) just to identify the manager. 5. **Classic `Component` class side-effects moved to `register-curly-component.ts`** - the `setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component)` and `Component.reopenClass({ positionalParams: [] })` calls no longer run at the top level of `component.ts`; they're in a side-effect file imported by `setup-registry.ts`. 6. **`DebugRenderTreeImpl` factory moved behind a registry** - `EnvironmentImpl` (in `@glimmer/runtime`) imported `DebugRenderTree` statically. New `registerDebugRenderTreeFactory` lets a side-effect module supply the constructor; `getDebugName` (the other static reach into `debug-render-tree`) moved to its own file. 7. **Lighter array predicate in `to-bool.ts`** - switched from `isArray` from `@ember/array` (which pulls the mixin / Enumerable / Observable / computed graph) to `Array.isArray(x) || isEmberArray(x)`. 8. **`contentFor` extracted to `runtime/lib/mixins/content-for.ts`** - `each-in.ts` no longer drags in the `ProxyMixin = Mixin.create(...)` graph just for an 8-line `contentFor` function. 9. **`@ember/instrumentation` hot path extracted to `lib/internal-instrument.ts`** - `_instrumentStart` and `flaggedInstrument` moved to a lib file; `subscribe` / `unsubscribe` / `instrument` machinery (dead code unless something subscribes) drops out of bundles that only use the hot path. 10. **`@ember/object`'s `action` decorator extracted to `@ember/object/action`** - moved the `action` decorator implementation behind its own deep import path so `@glimmer/component`-only apps don't pay for the @ember/object Mixin / CoreObject / Observable graph just to decorate handlers. 11. **Decouple VM debug symbols / names from `opcodes.ts`** - the opcode-name lookup tables and `LOCAL_DEBUG`-only debug brand metadata were statically imported from `@glimmer/runtime`'s opcode tables. Moved them out so prod builds can DCE them at the import level (not just the body level). 12. **`treeshake.moduleSideEffects` callback at the rollup level** - the package-level `sideEffects: false` declaration was getting lost when rolldown emitted shared chunks. Added a `PURE_INTERNAL_PACKAGES` list in `rollup.config.mjs` whose modules get `moduleSideEffects: false` even after chunking, so leaked debug code drops out of the renderer-only path entirely. 13. **Extract classic helper handler from resolver** - lazy-register the classic-helper detection (`isClassicHelper` + `CLASSIC_HELPER_MANAGER`) via a side-effect file imported from `setup-registry.ts`. Removes the static import of `./helper` from `resolver.ts`, which was pulling the classic Helper class chain (FrameworkObject -> CoreObject -> Mixin) into the renderer's path even when the app does not use any classic helpers. 14. **Decouple property_events / runloop / property_set from the observer chain** - replaced the static imports of `observer.ts` (sync flush) and `decorator.ts` (`COMPUTED_SETTERS`) with registration hooks; `observer.ts` and `decorator.ts` register themselves via top-level side effects when loaded. Drops `observer` / `events` / `chain-tags` / `decorator` chunks entirely from Glimmer-only bundles. 15. **`sideEffects` field on `ember-source/package.json`** - lists the files that actually have top-level side effects (registration files, environment / setGlobalContext callers, opcode handlers, runloop init, validator, etc.), which by inversion tells bundlers everything else is pure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 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.
Cuts the hello-world smoke test (`smoke-tests/v2-app-hello-world-template`) from 251.05 KB / 79.75 KB gzip → 174.28 KB / 55.76 KB gzip — a 30% gzip reduction — while leaving the classic `v2-app-template` building and tests passing.
The win comes from breaking specific dependency chains that pulled the classic-Ember-object stack (Mixin, Observable, Evented, classic Component, computed, observers, RSVP, routing) into the renderer-only path even when an app uses only `@glimmer/component` + `@tracked`.
Note
This is a re-application against `main` of the work originally landed as #21359 (which targets #21350). The deeper barrel-import refactor in #21350 is not included here; if both PRs land, a follow-up could go further by combining them — the current expectation on top of #21350 is hello-world at ~109 KB / ~34 KB gzip (-55%). This PR is the self-contained subset that works against `main` today.
Pattern
Three shapes recur:
Wrapped up with a `sideEffects` field on `ember-source/package.json` listing the actual side-effect files, so bundlers can tree-shake the rest of the graph aggressively.
Commits
Changes
1. Lazy `-mount` and `-outlet` keyword registration
`resolver.ts` no longer statically imports `mountHelper`/`outletHelper`. Replaced with `registerBuiltInKeywordHelper(name, helper)` and a side-effect file `syntax/register-routing-keywords.ts` imported by `setup-registry.ts`. Drops ~138 KB of `@ember/routing` + ~7 KB of `@ember/engine` from the renderer-only path.
2. Split classic `Renderer` subclass into `classic-renderer.ts`
Moved `Renderer extends BaseRenderer`, `ClassicRootState`, the concrete `DynamicScope` class, and the `View` interface out of `renderer.ts`. Added a `RootState` interface so `RendererState` is generic over either root type.
3. `RSVP.defer` → native `Promise` in `renderSettled`
Together with #1, lets the bundle drop the 62 KB rsvp shared chunk entirely.
4. Curly symbols extracted to `curly-symbols.ts`
`isCurlyManager` is now a brand check (`manager[CURLY_COMPONENT_BRAND] === true`) instead of an instance check, so the resolver no longer pulls `component-managers/curly.ts` (the full `CurlyComponentManager` lifecycle, ~17 KB) just to identify the manager.
5. Classic `Component` class side-effects moved to `register-curly-component.ts`
`setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component)` and `Component.reopenClass({ positionalParams: [] })` no longer run at the top level of `component.ts`; they're in a side-effect file imported by `setup-registry.ts`.
6. `DebugRenderTreeImpl` factory moved behind a registry
`EnvironmentImpl` (in `@glimmer/runtime`) imported `DebugRenderTree` statically. New `registerDebugRenderTreeFactory` lets a side-effect module supply the constructor; `getDebugName` (the other static reach into `debug-render-tree`) moved to its own file.
7. Lighter array predicate in `to-bool.ts`
Switched from `isArray` from `@ember/array` (which pulls the mixin / Enumerable / Observable / computed graph) to `Array.isArray(x) || isEmberArray(x)`.
8. `contentFor` extracted to `runtime/lib/mixins/content-for.ts`
`each-in.ts` no longer drags in the `ProxyMixin = Mixin.create(...)` graph just for an 8-line `contentFor` function.
9. `@ember/instrumentation` hot path extracted to `lib/internal-instrument.ts`
`_instrumentStart` and `flaggedInstrument` moved to a lib file; `subscribe`/`unsubscribe`/`instrument` machinery (dead code unless something subscribes) drops out of bundles that only use the hot path.
10. `@ember/object`'s `action` decorator extracted to `@ember/object/action`
Moved the `action` decorator implementation behind its own deep import path so `@glimmer/component`-only apps don't pay for the @ember/object Mixin / CoreObject / Observable graph just to decorate handlers.
11. Decouple VM debug symbols / names from `opcodes.ts`
The opcode-name lookup tables and `LOCAL_DEBUG`-only debug brand metadata were statically imported from `@glimmer/runtime`'s opcode tables. Moved them out so prod builds can DCE them at the import level (not just the body level).
12. `treeshake.moduleSideEffects` callback at the rollup level
The package-level `sideEffects: false` declaration was getting lost when rolldown emitted shared chunks. Added a `PURE_INTERNAL_PACKAGES` list in `rollup.config.mjs` whose modules get `moduleSideEffects: false` even after chunking, so leaked debug code drops out of the renderer-only path entirely.
13. Extract classic helper handler from resolver
Lazy-register the classic-helper detection (`isClassicHelper` + `CLASSIC_HELPER_MANAGER`) via a side-effect file imported from `setup-registry.ts`. Removes the static import of `./helper` from `resolver.ts`, which was pulling the classic Helper class chain (FrameworkObject → CoreObject → Mixin) into the renderer's path even when the app does not use any classic helpers.
14. Decouple property_events / runloop / property_set from the observer chain
Replaced the static imports of `observer.ts` (sync flush) and `decorator.ts` (`COMPUTED_SETTERS`) with registration hooks; `observer.ts` and `decorator.ts` register themselves via top-level side effects when loaded. Drops `observer` / `events` / `chain-tags` / `decorator` chunks entirely from Glimmer-only bundles.
15. `sideEffects` field on `ember-source/package.json`
Lists the files that actually have top-level side effects (registration files, environment / setGlobalContext callers, opcode handlers, runloop init, validator, etc.), which by inversion tells bundlers everything else is pure.
Test plan
🤖 Generated with Claude Code