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. The generic walk collects instance data while building a hashable shape key; serialization and HTML eligibility validation run only on the first mount of that shape. Repeated shapes also receive a guarded Python extraction routine, avoiding generic tree walking and prop classification on subsequent mounts. The routine checks structure and binding kinds and falls back when they change. Both caches are bounded, and instance handlers, refs, and expressions aren't retained.

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, VNode | None]], 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) -> MountPlan | None

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 guarded extraction routine handles repeated shapes. If none matches, a generic walk collects instance bindings and a shape key. The HTML string and eligibility verdict come 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/_hole/ _fragment).

required

Returns:

Type Description
MountPlan | None

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

What's in this module

template is the runtime analogue of SolidJS's compiled templates. A run-once component returns a tree whose structure is static; only holes, event handlers, refs, and reactive prop bindings change after mount. So the static skeleton is serialized to an HTML string once, registered with the kernel (REGISTER_TPL), and every later mount of the same shape is a single CLONE_TPL op. The kernel walks the clone in pre-order and assigns a dense block of node ids, which the Python side predicts with no read-backs.

Name Description
build_plan Serialize an element VNode's static structure, or return None when the tree must use per-node ops.
MountPlan The result: html, the pre-order order list, the dynamic bindings, and node_count.

Application code never calls these; the reconciler does.

How a plan is built

  • Static tags, attributes, classes, styles, and datasets go into the HTML string directly.
  • Static text is hoisted: it serializes as a one-space placeholder and is applied as a SET_TEXT binding after the clone. This is what lets a thousand list rows that differ only in their text share one skeleton.
  • Reactive props are recorded as bindings and wrapped in per-prop render effects after ids are assigned; event handlers become LISTEN ops; value, checked, and innerHTML are applied as DOM properties.
  • Dynamic children (holes, fragments, components) serialize as comment placeholders and are mounted at that position afterwards.
  • Plans are cached per shape: serialization and eligibility checks run on the first mount of a shape, and later structurally identical trees are a dictionary hit.

When the fast path is skipped

build_plan returns None, and the reconciler falls back to per-node ops (still batched into the same commit), when the HTML parser wouldn't reproduce the tree faithfully: fewer than three nodes, adjacent or empty text nodes, raw-text elements such as <script> and <textarea>, SVG and MathML subtrees (mounted with CREATE_ELEMENT_NS instead), invalid attribute names, or nestings the parser rewrites (bare text inside <table>, an implied <tbody>, an auto-closed <p>). The reconciler also skips templates when the backend can't parse HTML (a stub document without <template> support). The fallback is purely a performance difference; behavior is identical.

See also