Skip to content

Make DebugRenderTree tree-shakeable in production app builds#21483

Draft
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:tree-shakeable-debug-render-tree
Draft

Make DebugRenderTree tree-shakeable in production app builds#21483
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:tree-shakeable-debug-render-tree

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Summary

Makes the DebugRenderTree implementation tree-shakeable, so production builds of apps that never use it — like the v2-app-hello-world-template smoke-test app — no longer ship it.

Result for smoke-tests/v2-app-hello-world-template (vite build):

before after
bundle 152.75 kB 150.99 kB
gzip 48.49 kB 47.92 kB
DebugRenderTreeImpl in bundle yes (render-node:, captureRefs, … present) fully tree-shaken

Findings: why it always shipped

The hello-world app's only import is renderComponent from @ember/renderer. That path reaches glimmer's EnvironmentImpl, which did:

import DebugRenderTree from './debug-render-tree';
// ...
this.debugRenderTree = this.delegate.enableDebugTooling ? new DebugRenderTree() : undefined;

with enableDebugTooling coming from ENV._DEBUG_RENDER_TREE. Since EmberENV._DEBUG_RENDER_TREE is a runtime flag (documented as Ember Inspector's production opt-in, set via window.EmberENV before Ember evaluates), no bundler can ever prove the branch dead — so the implementation was retained in every production build, along with its bookkeeping cost being one undefined-check away on every render path.

The bytes themselves can't be conditionally present at runtime, so "don't ship it" necessarily means the production opt-in has to become graph-dependent: available when something in the app's module graph actually pulls in the debug tooling, absent otherwise.

What this PR does

Inverts the dependency: the environment no longer constructs the tree — its delegate supplies one (EnvironmentDelegate.enableDebugTooling: booleandebugRenderTree?: DebugRenderTree). The implementation is registered through a tiny factory seam in @ember/-internals/glimmer:

  • Debug builds register it eagerly (module-eval in @ember/-internals/glimmer/lib/environment.ts, inside if (DEBUG), which is stripped from dist/prod). _DEBUG_RENDER_TREE stays "always on in development" exactly as documented.
  • Production builds register it as a top-level side effect of importing @ember/debug/lib/capture-render-tree — the module the EmberENV._DEBUG_RENDER_TREE opt-in exists to serve (Ember Inspector calls captureRenderTree). Apps whose graph includes it (e.g. anything importing the @ember/debug barrel, including classic apps via the ember barrel) keep the documented production opt-in unchanged.

Apps whose production graph never reaches captureRenderTree — like the hello-world app — pay neither the bytes nor the bookkeeping. isArgumentCaptureError now keys off the presence of the tree, which matches the previous enableDebugTooling behavior.

Behavior change (the one caveat)

In a production build that tree-shakes captureRenderTree away, setting window.EmberENV = { _DEBUG_RENDER_TREE: true } no longer does anything — the code simply isn't there. That combination previously "worked" in the sense that the tree recorded, but such an app has no code path that could ever read it (captureRenderTree is the only consumer), so nothing observable is lost. Classic/barrel apps are unaffected.

Verification

  • pnpm build + pnpm type-check:internals pass; eslint + prettier clean on changed files.
  • Full headless test suite passes: 9396 tests, 0 failures (17 skips, as on main). Also verified with focused runs of the affected suites (filter=render tree 23/23 — covers both Application test: debug render tree and glimmer's Debug Render Tree suite; [integration] env; renderComponent 41/41).
  • dist/dev: eager registration confirmed present; dist/prod: @ember/-internals/glimmer no longer references the implementation, capture-render-tree.js retains the registration call (rollup's moduleSideEffects: false for @ember/debug does not strip it, and ember-source publishes no sideEffects: false, so app bundlers keep it too).
  • Runtime probes with headless Chrome against vite preview:
    • plain hello-world prod build renders (<body>hi </body>), zero render-tree markers in the bundle;
    • a probe variant that additionally imports captureRenderTree and sets EmberENV._DEBUG_RENDER_TREE = true in index.html renders correctly with the implementation shipped and live (152.83 kB — the cost returns only when actually opted in).

The glimmer-workspace DebugRenderTree test suite now provides the tree via a getter on the test env delegate (fresh instance per environment, mirroring EmberEnvironmentDelegate).

🤖 Generated with Claude Code

Previously, glimmer's EnvironmentImpl statically imported the
DebugRenderTree implementation and instantiated it behind the runtime
flag EmberENV._DEBUG_RENDER_TREE. Because that flag can be flipped at
runtime (Ember Inspector's production opt-in), no bundler could ever
prove the implementation unused, so every production app shipped the
render-tree bookkeeping code even when nothing could ever use it.

This inverts the dependency: the environment no longer constructs the
tree; its delegate supplies one. The implementation is registered via
a small factory seam in @ember/-internals/glimmer:

- In debug builds, registration happens eagerly (stripped from
  production builds), preserving _DEBUG_RENDER_TREE defaulting to on
  in development.
- In production builds, registration happens as a top-level side
  effect of importing @ember/debug/lib/capture-render-tree — the very
  module Ember Inspector's EmberENV._DEBUG_RENDER_TREE opt-in exists
  to serve. Apps that include captureRenderTree (e.g. via the ember
  barrel) keep the documented production opt-in.

Production apps whose module graph never reaches captureRenderTree pay
neither the bytes nor the bookkeeping. For the v2-app-hello-world
smoke test app, DebugRenderTreeImpl and friends are now fully
tree-shaken: 152.75 kB -> 150.99 kB (48.49 -> 47.92 kB brotli/gzip).

EnvironmentDelegate.enableDebugTooling (boolean) is replaced by an
optional debugRenderTree property; isArgumentCaptureError now keys off
the presence of the tree, matching the previous behavior.

Co-Authored-By: Claude Fable 5 <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