Status: accepted, and re-validated with measurements (2026-09-19)
The framework is specified as being built on native Web Components, and framework components must be able to use real custom elements as a host and to be exported as public web components. At the same time it must mount 100 000 rows competitively, and components are expected to be used as freely as functions.
customElements.define requires a class extending HTMLElement.connectedCallback — work a cloned template
subtree does not do..row > td breaks if a host sits between them).defineElement(C) or component(fn, { tag: true }) produces a
real custom element wrapping the same setup function.Option 3. The public behaviour is:
const Row = component(fn); // no host node, no upgrade cost
const Card = component(fn, { tag: true }); // <firsthand-card> host element
defineElement(Widget); // register an existing component
Both paths share the setup function, the reactive graph, the owner tree, props and disposal. A component’s identity is unchanged by hosting it.
The whole point, and now measured rather than assumed
(npm run bench:micro, raw data in benchmarks/results/micro.json).
Mounting 2000 components of the same component, Chromium, median of 12 repetitions after 3 warmups, variant order rotated:
| Variant | Mount | Update all | DOM nodes |
|---|---|---|---|
| hostless (default) | 15.35 ms | 10.20 ms | 2000 |
{ tag: true } |
24.25 ms (1.58x) | 9.80 ms | 4000 |
{ tag: true, shadow: true } |
26.45 ms (1.72x) | 10.10 ms | 2000* |
* the shadowed variant’s content lives in a shadow root, so a light-DOM node count does not see it; it is the same number of nodes plus 2000 shadow roots.
The original wording overstated the case. This ADR said the cost was “large enough to dominate a mass mount”. It is 1.5x-1.7x on mount, depending on the run — 1.51x when this table was first written, 1.58x on the latest — and nothing reliable at all on update, where the three variants have come out within a few per cent of each other in every direction. Real, and it doubles the node count, but it does not dominate. Both runs are in the git history of this file; neither is cherry-picked. The decision stands on the corrected number: paying 50 % more mount time and twice the DOM for a boundary most components do not need is the wrong default, and the applications that want it can ask for it per component.
An element host retains a DOM node, its internal element state and a reference from the custom element registry’s upgrade machinery. Hostless components retain only their owner object. Measured: 2000 components become 4000 DOM nodes instead of 2000 — exactly one extra node each.
Option 1 (universal custom elements): rejected on cost, now quantified at 1.5x-1.7x mount time and twice the DOM nodes. That is smaller than this ADR originally implied, and it is still the wrong thing to charge every application for by default. Option 2 (no custom elements): rejected because it violates an explicit requirement and removes the interop path.
Measured on Chromium only. If another engine turns out to make element upgrade
cheap, the number to re-run is npm run bench:micro.