Skip to content

Experiment: compile strict-mode templates directly to DOM operations - #21649

Draft
NullVoxPopuli wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:claude/gallant-heisenberg-aovzyr
Draft

NullVoxPopuli wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:claude/gallant-heisenberg-aovzyr

Conversation

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

Adds a prototype of compiling strict-mode templates straight to JavaScript, with no wire format, opcode compiler, or VMs:

  • @glimmer/compiler: generateDOM / compileToDOM (ASTv1 -> JS) and codegenBabelPlugin, which compiles template() calls from gjs/gts in place, so templates close over the module's JS bindings.
  • @glimmer/dom: the runtime helpers that compiled templates call (static HTML cloning, text/attributes/content, if/each/yield/let/ in-element, component/helper/modifier invocation through the existing managers, and a VM fallback for components without a compiled template).

Well-known built-ins (on, fn, hash, array, concat, get, eq, not, and, or, if, ...) are compiled inline. Tracking uses @glimmer/validator caches, so autotracking works unchanged.

Claude-Session: https://claude.ai/code/session_01H2cNyDMVxZb1xQB6oCzX89

claude added 3 commits October 2, 2026 21:20
Adds a prototype of compiling strict-mode templates straight to
JavaScript, with no wire format, opcode compiler, or VMs:

- @glimmer/compiler: `generateDOM` / `compileToDOM` (ASTv1 -> JS) and
  `codegenBabelPlugin`, which compiles `template()` calls from gjs/gts
  in place, so templates close over the module's JS bindings.
- @glimmer/dom: the runtime helpers that compiled templates call
  (static HTML cloning, text/attributes/content, if/each/yield/let/
  in-element, component/helper/modifier invocation through the existing
  managers, and a VM fallback for components without a compiled
  template).

Well-known built-ins (on, fn, hash, array, concat, get, eq, not, and,
or, if, ...) are compiled inline. Tracking uses @glimmer/validator
caches, so autotracking works unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2cNyDMVxZb1xQB6oCzX89
Switches both apps that the package-size report measures to the codegen
compiler:

- v2-app-hello-world-template: 132,266 -> 26,285 bytes of JS
  (41,801 -> 9,304 gzipped). Nothing from the VM is loaded.
- v2-app-template: 349,060 -> 356,809 bytes (110,323 -> 113,006 gzipped).
  It still needs the VM for the router and `{{outlet}}`.

To make that possible:

- ember-source now builds `@glimmer/dom` and `@glimmer/dom/vm`, and the
  ember-template-compiler entry exports `_codegenBabelPlugin` (runs in
  `pre`, so it goes before babel-plugin-ember-template-compilation).
- `@glimmer/dom` no longer imports the VM; it only sets up Ember's global
  context.
- New `@glimmer/dom/vm` (enabled with `vmInterop`): the VM can render
  compiled components (route templates, `{{outlet}}` via in-element so the
  outlet keeps its dynamic scope), and compiled templates can render
  VM-only components.
- The compiler supports Ember's allowed globals (RFC 1070), `(element)`,
  and reports `{{on}}` misuse at runtime with the same messages as the VM.

Smoke scenarios that use v2-app-template pass, except the
captureRenderTree test: compiled components don't report to the debug
render tree yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2cNyDMVxZb1xQB6oCzX89
Upstream now passes route templates their outlet as `@outlet` (with
`{{outlet}}` compiled to `<@outlet />`), and tears routes down differently,
which the smoke-test scenarios exercise.

- `{{outlet}}` in a compiled route template (with `vmInterop`) is an
  element that the bridge's VM template renders `<@outlet />` into (with
  in-element), so the outlet is in the same render tree as the route.
  Without `vmInterop`, it invokes `@outlet`.
- The island that a compiled component renders into is created by a helper
  with a destroyable, so the VM destroys it in the same pass as its own
  components (`willDestroy` used to run a scheduling round late).
- VM-only components (e.g. `<LinkTo>`) rendered from compiled templates get
  named args (curried), attributes and modifiers (forwarded to
  `...attributes`), and the default block. Their VM render is a destroyable
  child of the compiled block, and a new curried value (like a new
  `@outlet`) updates the VM render instead of replacing it.
- Components with no template at all don't need the VM.

All smoke scenarios based on v2-app-template pass, except the
captureRenderTree test (compiled components don't report to the debug
render tree yet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2cNyDMVxZb1xQB6oCzX89
@NullVoxPopuli-ai-agent
NullVoxPopuli-ai-agent force-pushed the claude/gallant-heisenberg-aovzyr branch from 7f3785c to af3aef7 Compare October 2, 2026 22:01
@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

📊 Size report

Tarball size — 1.2 MB → 1.3 MB

dist/dev   4%↑

File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/reactive/collections.js 20 kB / 3.4 kB -30%↓14 kB / -39%↓2.1 kB
./packages/@glimmer/dom/index.js — 18.4 kB / 5 kB
./packages/@glimmer/dom/vm/index.js — 10 kB / 2.9 kB
./packages/ember-template-compiler/index.js 4.1 kB / 1.4 kB 838%↑38 kB / 556%↑9.1 kB
./packages/shared-chunks/environment-BeWIimBE.js — 18.3 kB / 4.6 kB
./packages/shared-chunks/template-{hash}.js 1.1 kB / 401 B -54%↓491 B / -49.4%↓203 B
Total (Includes all files) 2.1 MB / 509.6 kB 4%↑2.2 MB / 4%↑531.3 kB

dist/prod   4%↑

File Before (Size / Brotli) After (Size / Brotli)
./packages/@ember/reactive/collections.js 20 kB / 3.4 kB -30%↓14 kB / -39.3%↓2 kB
./packages/@glimmer/dom/index.js — 18.4 kB / 4.9 kB
./packages/@glimmer/dom/vm/index.js — 10 kB / 2.9 kB
./packages/ember-template-compiler/index.js 3.2 kB / 1.1 kB 1,070%↑37.2 kB / 700%↑8.9 kB
./packages/shared-chunks/environment-DGccMupC.js — 16 kB / 3.8 kB
Total (Includes all files) 1.9 MB / 466.5 kB 4%↑2 MB / 5%↑488 kB

smoke-tests/v2-app-template/dist   3%↑

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 319.8 kB / 85.6 kB 3%↑330.3 kB / 4%↑88.6 kB
./assets/modules-{hash}.js 30.5 kB / 9.5 kB -3.68%↓29.4 kB / -3.75%↓9.2 kB
Total (Includes all files) 352.1 kB / 96 kB 3%↑361.5 kB / 3%↑98.6 kB

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

File Before (Size / Brotli) After (Size / Brotli)
./assets/main-{hash}.js 125.7 kB / 34.9 kB -78%↓27.6 kB / -74.9%↓8.8 kB
Total (Includes all files) 126.1 kB / 35.1 kB -77.8%↓28 kB / -74.5%↓8.9 kB

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

@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

red is because of missing debug render tree with the new output format

NullVoxPopuli-ai-agent pushed a commit to NullVoxPopuli-ai-agent/limber that referenced this pull request Oct 2, 2026
Vendors @glimmer/dom and the compiler from emberjs/ember.js#21649:

- compile with `vmInterop`, so compiled templates can render components
  that only have a VM template (e.g. from addons), and the other way around
- `@glimmer/dom/vm` is a known module (the VM interop runtime)
- allowed globals (RFC 1070) resolve in compiled templates
- the demo renders ember-primitives' <ExternalLink> from a compiled template

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2cNyDMVxZb1xQB6oCzX89
@NullVoxPopuli

NullVoxPopuli commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Bundle size check: limber's REPL with this experiment vendored in (gjs|codegen format), compared with limber main at 1c7c38dd (same ember-source, 7.5.0-alpha.3). Counts only the .js files in apps/repl/dist; brotli at quality 11.

base with codegen change
raw 6,065.2 KiB 6,099.8 KiB +34.6 KiB (+0.6%)
brotli 1,416.0 KiB 1,427.8 KiB +11.8 KiB (+0.8%)

Most of the growth is in three new lazily loaded chunks, about 9.4 KiB brotli in total: codegen-compiler (5.1 KiB), glimmer-dom (2.8 KiB) and glimmer-dom/vm (1.5 KiB). They only load when the codegen format is used.

without repl-sdk:

JS without repl-sdk Base Branch Change
Raw 5,997.1 KiB 6,014.4 KiB +17.3 KiB (+0.3%)
Brotli 1,396.1 KiB 1,402.6 KiB +6.5 KiB (+0.5%)

@NullVoxPopuli

Copy link
Copy Markdown
Contributor Author

on: NullVoxPopuli/limber#2287 (comment)

output looks like (in the repl):

import { clone as _clone, html as _html, at as _at, content as _content, get as _get, listen as _listen } from /*"@glimmer/dom"*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/190a098e-aaca-4511-8cf2-3e3dcf49710a';
import { template as _template, setTemplate as _setTemplate } from /*"@glimmer/dom/vm"*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/55ca1836-bc27-4f34-abe1-e39935d3307f';
const $_0_t2 = _html("<p>You have clicked the button <!> times.</p>\n\n<button type=\"button\">Click</button>");
import Component from /*'@glimmer/component'*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/b77d5391-da1e-4779-a569-0b244523d07c';
import { tracked } from /*'@glimmer/tracking'*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/62ba06ac-9fe4-4a2d-8221-c4d4c2d23003';
import { on } from /*'@ember/modifier'*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/c6d78410-ddf1-4133-89ec-6c48eca9b3bc';
import { g, i } from /*"decorator-transforms/runtime-esm"*/'blob:https://nvp-codegen-gjs-flavor.limber-glimdown.pages.dev/14686cdd-3aa5-464e-9221-35a1f369de76';
export default class HelloWorld extends Component {
  static {
    g(this.prototype, "count", [tracked], function () {
      return 0;
    });
  }
  #count = (i(this, "count"), void 0);
  increment = () => this.count += 1;
  static {
    _setTemplate(this, _template(($_b, $_ctx) => {
      const $_s = $_ctx.self,
        $_a = $_ctx.args;
      const $_0_r1 = _clone($_b, $_0_t2);
      const $_0_n3 = _at($_0_r1, 0, 1);
      const $_0_n4 = _at($_0_r1, 2);
      _content($_b, $_0_n3, () => _get($_s, "count"), 0);
      _listen($_b, $_0_n4, "click", () => _get($_s, "increment"));
      return $_0_r1;
    }));
  }
}
//# sourceURL=file:///src/index.gjs?v=4

This branch has not been deployed

No deployments
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