CHAPTER 10 – How HTML Works
Checking engine…

10.8 — DOM Interfaces & the C++ Binding Layer

The DOM you interact with in JavaScript is an illusion. The real DOM lives inside the browser’s C++ rendering engine. Every property access, every method call, crosses a bridge between two separate memory worlds. Understanding this binding layer, the inheritance chain, and the behaviour of live versus static collections is the key to unlocking true browser performance.

Diagram showing JavaScript wrapper objects linked to native C++ DOM nodes across a binding bridge
Figure 10.28: The JavaScript engine communicates with the C++ rendering engine through lightweight wrapper objects.

The JS‑to‑Engine Bridge

JavaScript’s memory space is completely isolated from the C++ engine that owns the DOM. When the parser creates a node, it instantiates a native C++ object. To let JavaScript touch that node, the engine creates a V8 wrapper (or equivalent) – a thin JavaScript object holding nothing but an internal pointer to the real C++ element.

Every property read or method call must cross this boundary. The wrapper marshals arguments, converts data types, and passes execution to the C++ side. The result then travels back. This crossing is not free; it introduces overhead. Tight loops that hit the bridge thousands of times can saturate the main thread and cause jank.

Diagram of the DOM interface inheritance chain: EventTarget → Node → Element → HTMLElement → specific elements
Figure 10.29: Every DOM object inherits from EventTarget, up through Node, Element, HTMLElement, to concrete implementations.

The Interface Inheritance Chain

Every DOM node sits on a precise rung of a strict inheritance ladder. From top to bottom:

  • EventTarget – the primal root. Provides addEventListener, removeEventListener, dispatchEvent. No concept of tags, text, or layout.
  • Node – adds structural tree relationships: parentNode, childNodes, nextSibling, and mutation methods like appendChild.
  • Element – narrows to tags; introduces attributes (getAttribute, setAttribute), class lists, and geometry (getBoundingClientRect, clientWidth).
  • HTMLElement – adds HTML‑specific global properties: tabIndex, hidden, dir, style, dataset.
  • Concrete Interfaces – leaf classes like HTMLInputElement (value, validation, selection) and HTMLTemplateElement (content, fragment) that implement the specific behaviour of each tag.

For example, HTMLTemplateElement stores its internal markup inside a detached DocumentFragment – a quarantined subtree that does not render, does not load resources, and does not execute scripts until cloned.

Diagram showing a live HTMLCollection constantly reflecting DOM changes vs a static NodeList that stays frozen
Figure 10.30: Live collections re‑query the DOM on every access; static collections freeze a snapshot.

Live vs. Static Collections

Methods like getElementsByClassName or the children property return an HTMLCollection – a live collection. It is not a static snapshot; it is a live window onto the current DOM. Each time you read .length or access an index, the engine may need to re‑walk the tree to ensure the result is up‑to‑date.

This becomes catastrophic inside loops that modify the DOM. Each iteration invalidates the collection’s internal cache, forcing a full re‑scan. This is known as synchronous layout thrashing.

In contrast, querySelectorAll returns a static NodeList. The engine walks the DOM once, gathers the matching wrappers, and freezes them. No matter how many elements you later add or remove, the NodeList remains unchanged, and accessing its length is a cheap, constant‑time operation.

// Live – slow if DOM changes during loop
const liveItems = document.getElementsByClassName('item');
for (let i = 0; i < liveItems.length; i++) {
    liveItems[i].classList.add('processed'); // dangerous re‑scan
}

// Static – safe and fast
const staticItems = document.querySelectorAll('.item');
staticItems.forEach(item => item.classList.add('processed'));
TAKEAWAY

The DOM is not a JavaScript object – it’s a C++ architecture wrapped in thin JS shells. Every access crosses a performance‑sensitive bridge. The inheritance chain (EventTarget → Node → Element → HTMLElement → specific interfaces) grants increasing capabilities. Use static collections (querySelectorAll) over live ones (getElementsByClassName) to avoid constant DOM re‑scans and layout thrashing. Mastering these internals lets you write code that feels native‑fast.

Checking engine…