- From: Rob Eisenberg <notifications@github.com>
- Date: Tue, 18 Nov 2025 17:57:05 -0800
- To: WICG/webcomponents <webcomponents@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <WICG/webcomponents/issues/1116/3550309345@github.com>
EisenbergEffect left a comment (WICG/webcomponents#1116)
Apologies for the upcoming long post...
In the event that it helps this discussion, I thought I would show what my template compiler takes as input and what its code emitters (roughly) produce as output. I'll demonstrate with a basic todo app template, which most folks should be familiar with, and which provides the opportunity to show a variety of templating constructs.
Here's the input template:
```html
<template>
<form @submit="this.addTodo($event)">
<input type="text" ref="description" placeholder="description">
<button type="submit">Add Todo</button>
</form>
<ul>
{#for todo of this.todos}
<li>
<input type="checkbox" ?checked="todo.done" @change="todo.done = !todo.done" title="done">
<span class="{todo.done && 'done'}">{todo.description}</span>
<button type="button" @click="this.removeTodo(todo)">X</button>
</li>
{/}
{#empty}
No todos...
{/}
</ul>
</template>
```
The important thing to note is that there are a variety of aspects of the DOM that the runtime needs to affect. It needs to:
* Add DOM events (`@submit`, `@click`, and `@change`)
* Capture element references (`ref`)
* Mark locations in the DOM to potentially insert/update HTML fragments. (`#for` and `#empty`)
* Mark locations in the DOM to potentially insert/update text. (`{...}`)
* Synchronize boolean attributes with state (`?checked="..."`)
* Synchronize content attributes with state (`class="{...}"`)
* Synchronize IDL attributes (properties) with state (not shown in this example, but imagine `:value` or `.value` similar to `?checked`)
Given this template, the compiler will produce a model that is passed to two different emitters, each of which produces an optimized output for its target context:
* Browser Emitter - Generates code that enables the template to be instantiated and data-bound entirely in the browser.
* Server Emitter - Generates HTML that can be streamed over HTTP and non-destructively hydrated in the browser.
Let's look at the output of the browser emitter first.
```js
const itemTemplate = __template(
`<li><input type="checkbox" title="done"><span><!----></span><button type="button">X</button></li>`,
r => {
const n = {};
n.description = r.firstChild.firstChild;
n.C = n.description.nextSibling;
n.D = __toText(n.C.firstChild);
n.E = n.C.nextSibling;
return n;
},
function(v, n) {
__boolAttrBehavior(v, n.description, () => this.todo.done, "checked");
__listener(v, n.description, () => this.todo.done = !this.todo.done, "change");
__propBehavior(v, n.C, () => __class(this.todo.done && 'done'), "className");
__contentBehavior(v, n.D, () => this.todo.description);
__listener(v, n.E, () => this.$root.removeTodo(this.todo), "click");
}
);
function createItemScope(p, o, k) {
const s = Object.create(null);
s.$root = p;
s.todo = o[k];
return s;
}
const emptyTemplate = __template(
`
No todos...
`
);
const mainTemplate = __template(
`<form><input type="text" placeholder="description" id="description"><button type="submit">Add Todo</button></form><ul><!----></ul>`,
r => {
const n = {};
n.A = r.firstChild;
n.description = n.A.firstChild;
n.B = n.A.nextSibling.firstChild;
return n;
},
function(v, n) {
__listener(v, n.A, $event => this.addTodo($event), "submit");
__forOfBehavior(v, n.B, () => this.todos, itemTemplate, this, createItemScope, void 0, emptyTemplate);
}
);
```
This may be a lot to absorb, but the key insight is that the runtime template has three distinct parameters:
1. The Raw HTML - This is the HTML that will be used to create a template instance at runtime.
2. The Node Locator - This is compile-time optimized code used to locate the DOM nodes that need behavior applied.
3. The Behavior Binder - This code applies behaviors to the previously located nodes. (e.g. adds listeners, sets up state sync, etc.)
Notice that in some cases the raw HTML has comments. These are used to mark locations where blocks or text need to be inserted or updated. Sometimes they are also used to provide a "stable" first child for a view (the compiler determines whether this is needed). But comments are not the best way to mark all nodes that need behavior. They are not without a cost, and in larger apps with many templates, the number of comments does have an effect on performance. As such, anything that is easily locatable directly, without a comment marker, is located that way, so that the minimal number of additional nodes is needed.
Comments play a critical role in today's browser rendering though. And they typically also have metadata associated with them so that behavior can be attached. But they aren't the optimal way to handle everything.
Now, let's look at the server render version of this same template. It's different because it has different needs and there are more efficient ways to produce this HTML in a request/response context.
NOTE: I'm showing debug output here because it's easier to read. Non-debug code combines yields as much as possible.
```js
function* itemTemplate(ctx, registry) {
yield `<!--${ctx.viewId}-->`;
yield `<li>`;
yield `<input data-r="description" data-v="${ctx.viewId}" type="checkbox" title="done" ${this.todo.done ? "checked" : ""}>`;
yield `<span data-r="C" data-v="${ctx.viewId}" class="${__class(this.todo.done && 'done')}">`;
yield `<!---->${__escapeText(this.todo.description)}<i-m data-r="D" data-v="${ctx.viewId}"></i-m>`;
yield `</span>`;
yield `<button data-r="E" data-v="${ctx.viewId}" type="button">`;
yield `X`;
yield `</button>`;
yield `</li>`;
}
function* emptyTemplate(ctx, registry) {
yield `<!--${ctx.viewId}-->`;
yield `
No todos...
`;
}
function createItemScope(p, o, k) {
const s = Object.create(null);
s.$root = p;
s.todo = o[k];
return s;
}
function* mainTemplate(ctx, registry) {
yield `<form data-r="A" data-v="${ctx.viewId}">`;
yield `<input data-r="description" data-v="${ctx.viewId}" type="text" placeholder="description" id="description"${ctx.renderRefAttributeHTML("description")}>`;
yield `<button type="submit">`;
yield `Add Todo`;
yield `</button>`;
yield `</form>`;
yield `<ul>`;
const $viewId = ctx.viewId;
const $var2 = this.todos;
if (!$var2 || $var2.length < 1) {
yield* ctx.renderComposition("B", emptyTemplate, this, registry);
} else {
for (let $var1 = 0; $var1 < $var2.length; ++$var1) {
ctx.pushViewId();
const $viewTargetId = ctx.viewId;
yield* ctx.renderTemplate(itemTemplate, createItemScope(this, $var2, $var1), registry);
ctx.popViewId();
if ($var1 === $var2.length - 1) {
yield `<i-m data-v="${$viewId}" data-r="B" data-vt="${$viewTargetId}"></i-m>`;
} else {
yield `<i-m data-vt="${$viewTargetId}"></i-m>`;
}
}
}
yield `</ul>`;
}
```
Ok, this is obviously quite a bit different. We don't need to split out raw html from node location and data-binding. These templates are just functions that emit bits of HTML into the response stream.
But there's again some important details because we need adequate metadata in order to be able to non-destructively hydrate (add the same behaviors without destroying nodes or unnecessarily writing to the DOM) in the browser. We use a combination of things to accomplish this:
* `data-v` - This attribute marks the view that a particular behavior-laden element is part of. We need this because, as you can see with the todo list rendering, we need to render the same template multiple times. And sometimes, we need to do this with a sequence of sibling nodes. We cannot assume that there is a singular parent for a view. So, this attribute marks which view instance each behavior-laden element is part of.
* `data-r` - This attribute indicates the node reference name. These are collected onto a lookup object so that the browser behavior binder can apply the behaviors.
* `<i-m ...>` - This element is a general marker, easily locatable via a query selector and serves as a relative anchor for locating other things.
* `data-vt` - Used to mark the end of a view. The runtime can traverse backwards from here to find the view start and gather nodes.
And yes, there are comments as well. These may be used to mark the start of a block of text so that the browser doesn't collapse nodes that are needed for rendering. They also mark a view's start.
So, again comments are important, but they aren't the only thing that plays a role in hydration in this example.
Regarding this proposal, I don't know that it's sufficient to replace all location-related code shown here. It may help, but it's less likely if I need to create a tree walker to find the comments. Particularly in the browser render scenario, I've already got an optimized path for node location.
Would this new approach be able to provide similar performance to my compiled output? Would it introduce complication into the non-destructive hydration code and would there be enough benefit? Perhaps I could remove the `<i-m>` elements and use comments for those, but is it worth it?
I don't know the answers to these questions. But I wanted to share something concrete from a real framework so folks can understand at least one way that these scenarios are being approached today.
--
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1116#issuecomment-3550309345
You are receiving this because you are subscribed to this thread.
Message ID: <WICG/webcomponents/issues/1116/3550309345@github.com>
Received on Wednesday, 19 November 2025 01:57:09 UTC