Skip to content

Template

wybthon.template

template

Template-based mounting: static HTML serialization plus hole wiring.

This module is the runtime analogue of SolidJS's compiled templates. A run-once component returns a VNode tree whose structure is static; only reactive holes, event handlers, refs, and reactive prop bindings change after mount. That means the static skeleton can be serialized to an HTML string in Python (cheap) and registered with the rendering kernel once; every mount of the same skeleton is a single CLONE_TPL op that clones the pre-parsed tree natively. The kernel then walks the clone in document order, assigning a dense block of node ids.

Because the Python serializer counts nodes in exactly the same pre-order the kernel walks, every element, text node, and placeholder comment gets a predictable id with zero extra communication: the mount simply assigns first_id + k to the k-th serialized node.

Static text content is hoisted out of the HTML: the serializer emits a one-space placeholder text node and records the real value as a SET_TEXT binding applied after the clone. Hoisting is what makes templates shared: a thousand list rows that differ only in their text (ids, labels) produce the same skeleton string, so the browser parses it once and clones it a thousand times.

The pipeline:

  1. build_plan walks a VNode tree and produces a MountPlan: the serialized HTML, the pre-order node list (for id assignment and text bindings), the dynamic bindings (events, reactive props, refs, DOM-property writes), or None when the tree isn't eligible for the fast path.
  2. The reconciler registers the HTML (once per unique skeleton), allocates the id block, emits the CLONE_TPL op, applies bindings by id, and mounts dynamic children (holes, fragments, components) at their placeholder comments.

Trees fall back to per-node ops (still batched, still one bridge crossing) when they contain constructs the HTML parser would mangle: adjacent or empty text nodes, raw text elements, invalid attribute names, or element nestings the parser rewrites (implied <tbody>, auto-closed <p>, and similar).

Plans are cached per shape: a single walk of the VNode tree collects the per-instance data (id order and bindings) while building a hashable shape key that uniquely determines the serialized HTML. Serialization, escaping, and eligibility validation run only on the first mount of each shape; every later mount of a structurally identical tree (for example, the rows of a list) is a dictionary hit.

Classes:

Name Description
MountPlan

Serialized mount plan for a static-skeleton VNode subtree.

Functions:

Name Description
build_plan

Serialize vnode's static structure, or return None when ineligible.

MountPlan

MountPlan(html: str, order: List[Tuple[int, VNode, Optional[VNode]]], bindings: List[Tuple[VNode, int, str, Any]])

Serialized mount plan for a static-skeleton VNode subtree.

Attributes:

Name Type Description
html

The serialized HTML string for the skeleton. Static text content is hoisted (each text node appears as a single-space placeholder), so structurally-identical trees share the same string regardless of their text.

order

Pre-order list of (kind, vnode, parent_vnode) entries, one per serialized DOM node, in exactly the order the kernel assigns ids. parent_vnode is the enclosing element VNode (needed to mount placeholders), or None for the root.

bindings

List of (vnode, kind, name, value) tuples to apply after ids are assigned.

node_count property

node_count: int

Number of serialized DOM nodes (length of the id block).

build_plan

build_plan(vnode: VNode) -> Optional[MountPlan]

Serialize vnode's static structure, or return None when ineligible.

Eligible trees have an element root and contain only element/text nodes plus dynamic placeholders (holes, fragments, components). The VNode tree is normalized in place (children lists become VNode lists) as a side effect, exactly as the per-node mount path does.

A single walk collects the per-instance order and bindings while building the shape key. The HTML string (and the eligibility verdict) comes from the shape cache; the full serializer runs only on the first mount of each shape.

Parameters:

Name Type Description Default
vnode VNode

An element VNode (string tag, not _text/_dynamic/ _fragment).

required

Returns:

Type Description
Optional[MountPlan]

A MountPlan, or None when the tree must use per-node mounting.

What's in this module

template implements the template-based mounting fast path. When the reconciler mounts a host-element subtree, it first asks build_plan to serialize the static skeleton of the tree into a single HTML string (with text content hoisted out). The skeleton is registered with the rendering kernel once, parsed by the browser via a <template> element, and every mount is a single clone op. The kernel walks the clone in a deterministic pre-order, assigning a dense block of node ids, so Python can address every node with no read-backs; text, bindings, and dynamic children are then wired by id in the same batched commit.

Hoisting text is what makes templates shared: a thousand list rows that differ only in their ids and labels serialize to the same skeleton, so the browser parses it once and clones it a thousand times, exactly like SolidJS's compiled templates.

How a plan is built

  • Static tags, attributes, classes, styles, and datasets become part of the HTML string directly.
  • Static text serializes as a one-space placeholder; the real value is recorded as a SET_TEXT binding applied after the clone.
  • Reactive props (getter callables) are recorded as bindings; they're wrapped in per-prop effects after id assignment.
  • Event handlers (on_*) are recorded as bindings and registered through the kernel's delegated event system (LISTEN ops).
  • value/checked are recorded as DOM-property bindings and assigned post-clone, matching the per-node mount path exactly.
  • Dynamic children (holes, components, fragments) are serialized as comment placeholders; the reconciler mounts them at the placeholder position after id assignment.

When the fast path is skipped

build_plan refuses (and the reconciler falls back to per-node ops, still batched in the same commit) when the subtree can't be represented faithfully as HTML, for example: raw-text elements like <script>, adjacent text nodes that would merge during parsing, element nestings the parser rewrites (bare text in <table>, auto-closed <p>, and similar), or environments whose DOM stub has no <template> support. The fallback is purely a performance difference; behavior is identical.

See also