Skip to content

Shrink hello-world bundle to 109 KB / 34 KB gzip / 30 KB brotli (-29%)#21387

Open
NullVoxPopuli-ai-agent wants to merge 14 commits intoemberjs:nvp/configure-side-effectsfrom
NullVoxPopuli-ai-agent:claude/shrink-hello-world-on-side-effects
Open

Shrink hello-world bundle to 109 KB / 34 KB gzip / 30 KB brotli (-29%)#21387
NullVoxPopuli-ai-agent wants to merge 14 commits intoemberjs:nvp/configure-side-effectsfrom
NullVoxPopuli-ai-agent:claude/shrink-hello-world-on-side-effects

Conversation

@NullVoxPopuli-ai-agent
Copy link
Copy Markdown
Contributor

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

Re-applies the PR #21359 / #21360 structural source-file changes on top of nvp/configure-side-effects. Cuts the smoke-tests/v2-app-hello-world-template bundle to 109.03 KB / 34.43 KB gzip / 30.04 KB brotli — a 29% gzip reduction vs. this branch, 55% vs. upstream main.

hello-world raw hello-world gzip hello-world brotli
upstream main 242.69 KB 77.13 KB 66.48 KB
this branch (nvp/configure-side-effects baseline) 151.92 KB 48.45 KB 42.17 KB
this PR 109.03 KB 34.43 KB 30.04 KB

(raw / gzip from vite build output; brotli from zlib.brotliCompressSync with default options.)

The bundler hints from #21362 (sideEffects: false, the treeshake.moduleSideEffects callback for @glimmer/debug{,-util,-flags}/@glimmer/env/@ember/debug) and the agadoo regression check are already on this branch. This PR is the structural complement: 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

History note: previous attempts at this work (#21362, #21360, #21359) were closed because the base branches they targeted were rebased/superseded. This PR is the same structural changes re-applied on the now-current upstream nvp/configure-side-effects. The bundler-hints subset already merged (the work in #21366 / #21367 / c0de6063f6); this PR adds the source-file restructuring on top.

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

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

12. 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.

13. sideEffects field on ember-source/package.json

Replaces the sideEffects: false declaration with an explicit list of files that actually have top-level side effects (registration files, environment / setGlobalContext callers, opcode handlers, runloop init, validator, etc.) — necessary now that side-effect-only registration files (register-curly-component.ts, register-routing-keywords.ts, debug-render-tree-register.ts) exist and must be preserved when imported only for their side effects.

What's left in the hello-world bundle

After all of the above, the remaining 109 KB raw / 34 KB gzip is essentially just genuine VM runtime + the bare minimum @ember internals: opcode compiler, VM argument handling, VM render loop, renderer setup/environment, backburner + runloop, element-builder, meta/owner/container.

Almost all of the previously-leaked classic-Ember-object machinery (Mixin, Observable, Evented, Component, computed properties, observers, Helper, RSVP, routing) is now gone from the renderer-only path.

Updated tree-shakability snapshot

The new side-effect-free entry points (@ember/-internals/runtime/lib/mixins/content-for, @ember/instrumentation/lib/internal-instrument, @ember/object/action) appear in the shaken snapshots in both dev and prod, confirming agadoo recognizes them as pure.

Test plan

  • pnpm build:js clean
  • pnpm test:node 20/20
  • pnpm --filter ember-test-node-vitest test:node 2/2 (tree-shakability snapshot)
  • smoke-tests/v2-app-template (classic v2 app) builds + 1/1 test passes
  • smoke-tests/app-template (v1 app) builds + 1/1 test passes
  • smoke-tests/v2-app-hello-world-template builds and shrinks as reported
  • pnpm vite build --mode development --minify false (full dev test suite app) builds clean
  • pnpm lint:eslint clean for tracked PR files
  • Browser tests pass in CI

🤖 Generated with Claude Code

NullVoxPopuli and others added 14 commits May 8, 2026 01:43
…ords

Cuts the hello-world smoke test from 243.30 KB / 77.32 KB gzip to
168.59 KB / 53.67 KB gzip — a 30.6% gzip reduction — while leaving the
classic v2-app-template essentially flat (+0.21 KB gzip from one extra
side-effect import).

Three changes, in order of impact:

1. **Lazy `-mount` and `-outlet` keyword registration.** Until now
   `resolver.ts` statically imported `mountHelper` and `outletHelper`,
   which transitively pulled `@ember/engine/instance`,
   `@ember/routing/-internals` (for `generateControllerFactory`), and the
   rest of the routing/engine graph into every bundle that uses
   `renderComponent`. Replace the static import with a
   `registerBuiltInKeywordHelper(name, helper)` registry on the resolver,
   and add a side-effect-only `syntax/register-routing-keywords.ts` that
   classic-app setup imports from `setup-registry.ts`. Bundles that don't
   pull in `setup-registry` (i.e. the hello-world that only uses
   `@ember/renderer`) drop ~138 KB of routing + ~7 KB of engine code.

2. **Split classic `Renderer` subclass into `classic-renderer.ts`.**
   Move `Renderer extends BaseRenderer`, `ClassicRootState`, the concrete
   `DynamicScope` class, and the `View` interface out of `renderer.ts`.
   Hoists the imports those carry — `OutletView`, `createRootOutlet`,
   `RootComponentDefinition`, `makeRouteTemplate`, `renderMain`,
   `guidFor`, `getViewElement`, `getViewId`, `dict`, `createCapturedArgs`,
   `EMPTY_POSITIONAL`, `curry` — out of the renderer-only bundle. Adds a
   `RootState` interface so `RendererState` can manage either kind
   without statically depending on classic code. `setup-registry.ts` now
   imports `Renderer` from `./classic-renderer`. The renderer entry
   re-exports the classic types so existing `from '.../renderer'`
   import sites keep working.

3. **Replace `RSVP.defer` in `renderSettled` with native Promise.**
   Standalone this didn't move the bundle (rsvp was reachable via other
   paths), but together with #1 it lets the hello-world bundle drop the
   62 KB rsvp shared chunk entirely — `@ember/engine`, `@ember/routing`,
   and `@ember/-internals/runtime/lib/ext/rsvp` were the remaining
   consumers, and #1 pulls those off the renderer-only path.

Verified: `lint:eslint`, `type-check:internals`, `type-check:types`,
`type-check:handlebars`, `test:node`, `test:blueprints`, classic
v2-app-template build, hello-world build, and a vite dev build of the
full test suite all pass. Browser tests will run in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds on the previous classic-renderer/routing-keywords split. The
hello-world smoke test goes from 168.59 KB / 53.67 KB gzip to
160.11 KB / 50.88 KB gzip. Cumulative with prior commit: 243.30 KB /
77.32 KB → 160.11 KB / 50.88 KB (34.2% gzip reduction). Classic
v2-app-template stays flat (319.55 KB / 99.38 KB).

Each change is the same registration pattern as before — a separate
side-effect file imported by classic-app `setup-registry`, leaving the
heavy module out of the renderer-only path.

1. **Curly symbols extracted to `curly-symbols.ts`.** `BOUNDS`,
   `DIRTY_TAG`, `IS_DISPATCHING_ATTRS`, and the new `CURLY_COMPONENT_BRAND`
   live in their own file. `isCurlyManager` is now a brand check
   (`manager[CURLY_COMPONENT_BRAND] === true`) instead of an instance
   check, so the resolver no longer pulls in `./curly` (the full
   `CurlyComponentManager` lifecycle, ~17 KB) just to identify the
   manager. `curly.ts` re-exports the symbols for back-compat and tags
   `CURLY_COMPONENT_MANAGER` with the brand. `classic-renderer.ts` and
   `resolver.ts` now import from `curly-symbols.ts`.

2. **`@ember/-internals/glimmer/lib/component`'s top-level side effects
   moved to `register-curly-component.ts`.**
   `setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component)` and
   `Component.reopenClass({ positionalParams: [] })` ran at module load
   time, which kept the full classic `Component` class graph reachable
   from anything that imported `@ember/component` (e.g.
   `@glimmer/component`'s `setComponentManager`/`capabilities` imports).
   The registration now lives in a side-effect-only file imported by
   `setup-registry.ts`, so classic apps still get it on boot.

3. **`DebugRenderTreeImpl` factory moved behind a registry in
   `@glimmer/runtime/.../environment.ts`.** Previously `EnvironmentImpl`
   imported `DebugRenderTree` statically and only constructed one when
   `delegate.enableDebugTooling` was true — but the import alone pulled
   the whole class (and its `getDebugName` cousin) into every bundle.
   New `registerDebugRenderTreeFactory` lets a side-effect module
   (`debug-render-tree-register.ts`) supply the constructor; without
   that import, `env.debugRenderTree` stays `undefined` even when the
   delegate flag is set. Classic apps re-register it via
   `setup-registry.ts`. `getDebugName` was the other static reach into
   `debug-render-tree`, so it moved to its own file (`get-debug-name.ts`)
   that opcodes can import without dragging the rest in.

4. **`to-bool.ts` swapped `isArray` from `@ember/array` for
   `Array.isArray(x) || isEmberArray(x)`.** `isArray` from
   `@ember/array` calls `EmberArray.detect`, which transitively pulls
   `@ember/array`'s entire mixin/Enumerable/Observable/computed graph
   (~16 KB) just to test array-ness inside `{{#if}}`. Using
   `isEmberArray` from `@ember/array/-internals` (a WeakSet brand set
   in `EmberArray#init`) covers all instances of EmberArray-mixed
   classes — the same set the old check covered in practice.

Verified: lint clean, all type-checks pass, `test:node` 20/20,
`test:blueprints` 265/265, both smoke-test apps build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more lazy-load splits on top of the previous round. Hello-world goes
from 160.11 KB / 50.88 KB → 159.49 KB / 50.67 KB. The classic
v2-app-template also gets a small win (319.55 → 318.27 KB raw,
99.38 → 98.94 KB gzip).

Cumulative from baseline: 243.30 KB / 77.32 KB → 159.49 KB / 50.67 KB
(34.5% gzip reduction).

1. **`contentFor` extracted to `runtime/lib/mixins/content-for.ts`.**
   `each-in.ts` (which the renderer registers as the `-each-in` keyword
   helper) imports `contentFor` to unwrap proxies before iterating.
   Until now that import dragged in `runtime/lib/mixins/-proxy`, which
   defines `ProxyMixin = Mixin.create(...)` at module scope — the entry
   point to the entire EmberObject / Mixin / computed graph (proxy.ts
   imports `Mixin`, `computed`, `defineProperty`, `set`, etc.). Moving
   the 8-line `contentFor` function into its own file lets the renderer
   path keep proxy support without paying for the rest of the proxy
   mixin's transitive imports. `-proxy.ts` re-exports `contentFor` from
   the new file for back-compat.

2. **`@ember/instrumentation` hot path extracted to
   `instrumentation/lib/internal-instrument.ts`.** `_instrumentStart`
   (called by the resolver, curly manager, outlet/root/route-template
   managers) and `flaggedInstrument` (called by the views state
   machine) used to live in `index.ts` alongside `subscribe`,
   `unsubscribe`, `instrument`, etc. — most of which are dead code
   unless something actually subscribes (e.g. Ember Inspector). Moved
   `_instrumentStart`, `flaggedInstrument`, `subscribers`, the cache
   helpers, and `NOOP` to the lib file, with `index.ts` re-exporting
   them via `export ... from`. The `no-barrel-imports` autofix then
   rewrites internal callers to deep-import from the lib file. Net
   result: the `instrumentation/index.js` chunk (subscribe / unsubscribe
   / instrument machinery) drops out of bundles that only use the hot
   path.

`package.json`'s `ember-addon.renamed-modules` map gains an entry for
`runtime/lib/mixins/content-for.js` — that's emitted automatically by
the `packageMeta` rollup plugin, no manual edit.

Verified: lint clean, all type-checks pass, `test:node` 20/20,
`test:blueprints` 265/265, both smoke-test apps build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hello-world: 159.49 KB / 50.67 KB → 134.17 KB / 42.90 KB gzip
(–25 KB raw / –7.8 KB gzip).
Classic v2-app-template gets a small bonus too (319.55 → 317.74 KB raw,
99.38 → 98.77 KB gzip).

Cumulative from the original 243.30 KB / 77.32 KB baseline: a
**44.5% gzip reduction**.

Added a `sideEffects` field to `ember-source/package.json` listing the
files that actually have top-level side effects, which by inversion
tells bundlers that everything else is side-effect-free. With the
classic-renderer / register-curly-component / register-routing-keywords
splits already done in this PR, the renderer-only path no longer reaches
into any of the side-effect files, so vite/rolldown can drop the rest of
the graph it pulled in transitively (mostly the classic `Component`
class and its CoreView/Mixin chain that vite was previously evaluating
via `@ember/component`'s `default` re-export).

The list covers:

- **Registration modules created in this PR** (`setup-registry*`,
  `register-routing-keywords*`, `register-curly-component*`,
  `debug-render-tree-register*`) — these mutate global state on
  import.
- **`environment*` files** (in `@ember/-internals/glimmer/` and
  `@glimmer/runtime/`) — call `setGlobalContext(...)` and the
  `_backburner.on(...)` lifecycle hookups at module top level.
- **`@glimmer/runtime/lib/compiled/opcodes/**`** — every opcode file
  registers handlers via `APPEND_OPCODES.add(...)` at module load.
- **`@glimmer/runtime/lib/helpers/**` and `lib/modifiers/**`** —
  setHelperManager / setModifierManager calls.
- **`@ember/-internals/glimmer/lib/components/**`** — Input, Textarea,
  LinkTo all call `setInternalComponentManager(...)` at top level.
- **`runtime/lib/component/template-only*`, `runtime/lib/vm/low-level*`** —
  template-only manager registration and VM bootstrap.
- **`runloop/`, `manager/`, `validator/`, `global-context/`,
  `destroyable/`, `canary-features/`, `-internals/environment/`,
  `-internals/runtime/lib/ext/rsvp*`,
  `-internals/views/lib/system/event_dispatcher*`** — top-level side
  effects in those modules' index/init files.
- **`./dist/dev/**`** — keep dev builds maximally unmolested for
  inspector / debugging tooling that may rely on dev-only side effects.

Anything outside that list — class-definition files like `component.ts`,
`core_view.ts`, `core.ts`, mixin files, computed-property files — is
treated by bundlers as pure, so unused exports drop out cleanly.

Verified: lint clean, all type-checks pass, `test:node` 20/20,
`test:blueprints` 265/265, `pnpm test` for both
`smoke-tests/v2-app-template` (classic v2 app) and
`smoke-tests/app-template` (v1 app) pass 1/1 each, hello-world builds
and shrinks as reported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI lint caught these without the local eslint cache: the
no-barrel-imports rule wants `flaggedInstrument` imported from
`@ember/instrumentation/lib/internal-instrument` (the actual source)
rather than the barrel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `addMixin` / `hasMixin` / `forEachMixins` methods only existed on
`Meta` to be called by `@ember/object/mixin`. Keeping them on the class
forced a static reference from `Meta` (reachable from the renderer
through the property accessor / tag chain) into the classic `Mixin`
graph.

Move them out as standalone functions (`metaAddMixin` / `metaHasMixin` /
`metaForEachMixins`) in `mixin.ts` itself, poking at `Meta`'s public
`_mixins` and `parent` fields directly. With this split, bundles that
don't import `@ember/object/mixin` get a cleaner `Meta` class — in the
hello-world prod bundle the `addMixin` / `hasMixin` / `forEachMixins`
identifiers go from present to fully absent, and `Mixin` references
drop from 12 to 5.

The methods were `@internal` and only called from `mixin.ts`, so this
is a purely internal refactor.

Verified: lint clean, type-checks pass, hello-world builds at
134.19 KB / 42.94 KB gzip (unchanged), classic v2-app-template tests
1/1 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of polish from the user's review:

1. The `sideEffects` field in package.json was over-broad — listing
   whole directories (`**/manager/**`, `**/validator/**`, etc.) when
   only a handful of files in those trees actually have top-level side
   effects. Replaced the directory globs with the explicit list of
   files that contain top-level calls (registrations, opcode
   `APPEND_OPCODES.add(...)`, `setGlobalContext`, `_backburner.on`,
   etc.). Hello-world stays at 134.12 KB / 42.92 KB.

2. Removed comments in the refactored files that explained the
   refactor itself ("extracted from X for tree-shaking", "kept
   separate so Y", "back-compat re-export"). That kind of context
   belongs in the PR description and commit messages, not the source
   tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`opcodes.ts` previously imported `@glimmer/debug` (DebugLogger,
VmSnapshot, debugOp / describeOp, opcodeMetadata, frag, etc.) at the
top level and assembled the per-opcode `debugBefore`/`debugAfter`
hooks inline in `AppendOpcodes`'s constructor — gated by `LOCAL_DEBUG`,
so dead in production, but the imports still pulled the heavy
`@glimmer/debug` graph into the bundle.

Same registration pattern as the DebugRenderTree split: opcodes.ts
exposes `registerDebugOpcodeSetup(setup)`; the heavy hook
implementation moved to `opcodes-debug-setup.ts`, which calls the
registry on import. `externs(vm)` now also requires the hooks to be
registered (returns `undefined` otherwise) so dev builds that don't
opt in skip the debug path entirely instead of crashing on a non-null
assertion.

Production hello-world holds at 134.12 KB / 42.90 KB gzip (`LOCAL_DEBUG`
already eliminated the hooks there); the analysis bundle drops the
`@glimmer/debug` files entirely.

Verified: lint clean (after `pnpm lint:fix`), type-checks pass,
hello-world builds, classic v2-app-template `pnpm test` 1/1 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pnpm lint:fix` runs both `lint:eslint:fix` and `lint:format:fix`.
I'd only been running the eslint half, so prettier formatting drift
in five of my refactored files snuck through. CI's `lint:format` was
the failure on the previous push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `action` decorator lived inline in `@ember/object/index.ts`, which
also imports `CoreObject` and `Observable` at module top — so any
component that pulled `import { action } from '@ember/object'` (Input,
Textarea, AbstractInput, LinkTo) dragged the full
EmberObject / Observable / Mixin graph along with it.

Move `action` (plus its `setupAction` helper, `BINDINGS_MAP`, and
`hasProto`) to `@ember/object/action.ts`. `index.ts` re-exports it via
`export { action } from './action'` so the no-barrel-imports lint
autofix rewrites internal call sites to the deep path.
`@ember/object/index.ts` itself loses its references to
`isElementDescriptor` / `setClassicDecorator` / `ElementDescriptor` /
`ExtendedMethodDecorator`, since those now live in `action.ts`.

Hello-world: 134.12 KB / 42.94 KB → 133.42 KB / 42.69 KB gzip.

Verified with `pnpm lint` (clean), `pnpm type-check:internals`,
hello-world build, classic v2-app and v1 app smoke-test `pnpm test` (1/1 each).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per a measurement pass, two commits had zero (or negative) effect on the hello-world prod bundle:

- a0b1f09 (Move Meta mixin methods to standalone fns): bundle went from 134.17 KB → 134.19 KB (+0.02 KB). Mixin.create chain was already being tree-shaken in prod regardless.
- 75761b8 (Decouple VM debug symbols/names from opcodes.ts): bundle held at 134.12 KB. LOCAL_DEBUG=false in dist/prod (and dist/dev) was already constant-folding the debug branches out, and vite was already tree-shaking the unused @glimmer/debug imports out of the smoke-test bundle.

Both refactors were architecturally cleaner but pure no-ops at the bundle measurement that motivated this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lazy-register the classic-helper detection (`isClassicHelper` +
`CLASSIC_HELPER_MANAGER`) via a side-effect file imported from
`setup-registry.ts`, the same pattern already used for routing
keywords / curly components / debug-render-tree.

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.

hello-world bundle: 131.27 KB → 114.92 KB raw (-16.35 KB),
                     42.06 KB →  36.43 KB gzip (-5.63 KB).
classic v2-app builds unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The renderer-only path was statically pulling in:
  - observer.ts (sync + async observer flush)
  - chain-tags.ts (transitively, for getChainTagsForKey)
  - events.ts (transitively, for addListener/sendEvent)
  - decorator.ts (for COMPUTED_SETTERS)

…even though a Glimmer-only app never installs an observer or a
classic computed setter. Three independent reach points were
responsible:

1. property_events.ts -> observer (sync flushSyncObservers etc.)
2. runloop/index.ts -> observer (async flushAsyncObservers)
3. property_set.ts -> decorator (COMPUTED_SETTERS WeakSet)

Replaced each direct import with a registration hook
(`registerObserverFlushSync` / `registerObserverDeactivationHooks`,
`registerAsyncObserverFlush`, `registerComputedSetterCheck`) and
moved the wire-up to a top-level side effect in `observer.ts` and
`decorator.ts` themselves. Anyone importing those modules
(addObserver/removeObserver, @computed, etc.) gets the registration
fire as a side effect; renderer-only paths skip it.

Marked observer.ts and decorator.ts as side-effect files in the
ember-source `sideEffects` list so the registration calls survive
tree-shaking when the modules ARE loaded.

hello-world bundle: 114.92 KB -> 109.03 KB raw (-5.89 KB),
                     36.43 KB ->  34.44 KB gzip (-1.99 KB).
classic v2-app builds and tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the new side-effect-free files introduced by this PR:
- @ember/-internals/runtime/lib/mixins/content-for.js (extracted hot path)
- @ember/instrumentation/lib/internal-instrument.js (extracted hot path)
- @ember/object/action.js (decorator extracted from @ember/object)

All three appear in the "shaken" snapshots (dev + prod), confirming
they get fully tree-shaken when imported only for side effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NullVoxPopuli
Copy link
Copy Markdown
Contributor

📊 Size report

Tarball size1.2 MB1.2 MB

dist/dev   0.1%↑

File Before (Size / Brotli) After (Size / Brotli)
Total (Includes all files) 2 MB / 478.1 kB 0.1%↑2 MB / 0.3%↑479.6 kB
Show files (24 files)
File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/-internals/metal/index.js 4.4 kB / 1.3 kB 3%↑4.5 kB / 2%↑1.3 kB
./packages/@ember/-internals/runtime/lib/mixins/-proxy.js 3.9 kB / 1.3 kB -8.95%↓3.6 kB / -10.1%↓1.2 kB
./packages/@ember/-internals/runtime/lib/mixins/content-for.js 513 B / 241 B
./packages/@ember/component/index.js 44.4 kB / 10.2 kB -97.3%↓1.2 kB / -96.2%↓388 B
./packages/@ember/instrumentation/index.js 5.7 kB / 1.6 kB -27.9%↓4.1 kB / -25.2%↓1.2 kB
./packages/@ember/instrumentation/lib/internal-instrument.js 2.1 kB / 680 B
./packages/@ember/object/action.js 2.3 kB / 739 B
./packages/@ember/object/index.js 7.3 kB / 2 kB -51%↓3.6 kB / -45.3%↓1.1 kB
./packages/@ember/runloop/index.js 23.5 kB / 5.1 kB 0.6%↑23.6 kB / 0.1%↑5.1 kB
./packages/shared-chunks/chain-{hash}.js 7.6 kB / 2.2 kB -17.6%↓6.3 kB / -17.3%↓1.8 kB
./packages/shared-chunks/component-LzW-d3sh.js 77.8 kB / 17.3 kB
./packages/shared-chunks/computed-{hash}.js 34 kB / 7.9 kB -7.89%↓31.3 kB / -7.45%↓7.3 kB
./packages/shared-chunks/curly-{hash}.js 21.5 kB / 5.1 kB -98.1%↓400 B / -96.4%↓181 B
./packages/shared-chunks/decorator-{hash}.js 4.7 kB / 1.3 kB 3%↑4.8 kB / 4%↑1.4 kB
./packages/shared-chunks/index-{hash}.js 78.5 kB / 19.1 kB -20.4%↓62.5 kB / -34.5%↓12.5 kB
./packages/shared-chunks/object-at-Bps-ulVR.js 167 B / 95 B
./packages/shared-chunks/observers-{hash}.js 6.8 kB / 1.6 kB 4%↑7.1 kB / 5%↑1.6 kB
./packages/shared-chunks/property_events-Dpk8DRL6.js 3.6 kB / 1.1 kB
./packages/shared-chunks/property_set-{hash}.js 4.3 kB / 1.4 kB 3%↑4.5 kB / 0.8%↑1.4 kB
./packages/shared-chunks/render-{hash}.js 55.5 kB / 12 kB 0.3%↑55.7 kB / 0.3%↑12.1 kB
./packages/shared-chunks/setup-{hash}.js 2.8 kB / 904 B 1,340%↑40.1 kB / 1,030%↑10.2 kB
./packages/shared-chunks/tags-CMTUbJMI.js 1.5 kB / 587 B
./packages/shared-chunks/textarea-WSlUEr0y.js 14.5 kB / 3.5 kB
./packages/shared-chunks/untouchable-{hash}.js 62.6 kB / 12.2 kB -6.07%↓58.8 kB / -7.68%↓11.3 kB

dist/prod   0.1%↑

File Before (Size / Brotli) After (Size / Brotli)
Total (Includes all files) 1.8 MB / 436.1 kB 0.1%↑1.8 MB / 0.3%↑437.4 kB
Show files (25 files)
File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/-internals/metal/index.js 4.2 kB / 1.2 kB 3%↑4.3 kB / 2%↑1.3 kB
./packages/@ember/-internals/runtime/lib/mixins/-proxy.js 3 kB / 993 B -11.8%↓2.6 kB / -11.2%↓882 B
./packages/@ember/-internals/runtime/lib/mixins/content-for.js 513 B / 270 B
./packages/@ember/component/index.js 40.2 kB / 9.4 kB -97.3%↓1.1 kB / -96.1%↓367 B
./packages/@ember/instrumentation/index.js 5.5 kB / 1.6 kB -25.8%↓4.1 kB / -22.2%↓1.2 kB
./packages/@ember/instrumentation/lib/internal-instrument.js 1.9 kB / 638 B
./packages/@ember/object/action.js 1.5 kB / 566 B
./packages/@ember/object/index.js 5.7 kB / 1.7 kB -52.6%↓2.7 kB / -47%↓910 B
./packages/@ember/runloop/index.js 22.2 kB / 4.8 kB 0.6%↑22.3 kB / 0.3%↑4.9 kB
./packages/shared-chunks/arguments-D9HH7n3q.js 55 kB / 10.5 kB
./packages/shared-chunks/chain-{hash}.js 6.9 kB / 2 kB -12.9%↓6 kB / -12.5%↓1.7 kB
./packages/shared-chunks/component-C0g-p-9O.js 67.5 kB / 15.5 kB
./packages/shared-chunks/computed-{hash}.js 28 kB / 6.6 kB -7.99%↓25.8 kB / -8.14%↓6 kB
./packages/shared-chunks/curly-{hash}.js 17.2 kB / 4.3 kB -97.7%↓400 B / -95.8%↓181 B
./packages/shared-chunks/debug-render-tree-CF5O4-WI.js 58.4 kB / 11.3 kB
./packages/shared-chunks/decorator-{hash}.js 3.7 kB / 1.1 kB 4%↑3.8 kB / 5%↑1.1 kB
./packages/shared-chunks/index-{hash}.js 66.5 kB / 16.3 kB -40.8%↓39.4 kB / -38.7%↓10 kB
./packages/shared-chunks/object-at-Bps-ulVR.js 167 B / 95 B
./packages/shared-chunks/observers-{hash}.js 6.8 kB / 1.6 kB 4%↑7.1 kB / 4%↑1.6 kB
./packages/shared-chunks/property_events-CXghSZBR.js 3 kB / 951 B
./packages/shared-chunks/property_set-{hash}.js 3 kB / 982 B 5%↑3.1 kB / 3%↑1 kB
./packages/shared-chunks/render-{hash}.js 51.8 kB / 11.2 kB 0.3%↑52 kB / 0.3%↑11.2 kB
./packages/shared-chunks/setup-{hash}.js 2.6 kB / 858 B 1,240%↑34.8 kB / 944%↑9 kB
./packages/shared-chunks/tags-BYdj326j.js 982 B / 406 B
./packages/shared-chunks/textarea-CHqL0Rdh.js 12.5 kB / 3.2 kB

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

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 151.9 kB / 42.2 kB -28.2%↓109 kB / -28.8%↓30 kB
Total (Includes all files) 152.2 kB / 42.3 kB -28.2%↓109.4 kB / -28.7%↓30.2 kB

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

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