bi-04 — DOM Internals & Object Lifetime

Phase 1 · Spec areas §8 (DOM internals), part of §4 (Blink GC types). Prerequisites: bi-01, bi-02, bi-03.

Cross-track hook: frontend-principal-engineering.md §3 (HTML platform), §5 (React internals — the cost model every VDOM diff is optimising against lives here).


0. Orientation

The 60-second version

If you take one thing from this module: "the DOM tree" is ambiguous, and the ambiguity is where the bugs are.

The DOM is not one tree. It is a node tree, plus shadow trees attached to hosts, plus the composed/flat tree that results from slotting. Style and layout operate on the flat tree, not the node tree. Almost every confusing shadow-DOM behaviour — inheritance across boundaries, ::slotted, event retargeting — is a consequence of those being different trees.

The second structural fact: a <div> is two objects. A C++ HTMLDivElement on Oilpan's heap and a V8 wrapper on V8's heap. Neither collector can independently decide the pair is dead:

const d = document.createElement('div');
d.myExpando = 42;
document.body.appendChild(d);
// ... no JS reference retained
document.body.firstChild.myExpando  // must still be 42

Wrapper identity and expando survival is why the two heaps must be traced together rather than separately. It is a genuinely hard problem and it is why the bindings layer (bi-05) is as complicated as it is.

The third: attributes are not properties

Attributes are the serialised, spec-defined store; IDL attributes ("properties") are a reflected view with type coercion. Some reflect, some do not — input.value is the classic divergence, where the attribute is the default and the property is the current value. Frameworks that set properties and frameworks that set attributes behave differently on exactly these, which is a recurring source of "works in React, breaks in Vue"-shaped reports.

What you should be able to do after this module

CapabilityEvidence you have it
Say which treeName node vs shadow vs flat before reasoning about style, events, or focus
Reason about lifetimeExplain what keeps a node alive across two heaps
Predict reflectionKnow which attribute/property pairs diverge, and why
Choose an observerPick from the mutation-observation family by delivery timing

Why a Principal Engineer needs this

1. Every framework you evaluate is a bet about DOM cost. React's virtual DOM, Vue's dependency tracking, and signals' fine-grained updates are three different answers to "DOM mutation is expensive, so avoid it." You cannot judge those answers without knowing which operations are expensive and why. Most engineers carry a folk model here ("the DOM is slow") that is wrong in detail and therefore produces wrong optimisations.

2. Memory leaks in long-lived SPAs are DOM lifetime bugs. Detached subtrees kept alive by a JS reference, listeners never removed, observers never disconnected. Diagnosing these requires knowing how DOM objects are owned and when they die — which spans two garbage collectors that must agree.

3. Invalidation starts here. Every style, layout and paint recalculation is triggered by a DOM mutation. bi-07–bi-09 are downstream of this module; "why did this cause a relayout" is a question about what the mutation marked dirty.


Mental Model

The tree, and the several trees

Document
└── Element (html)
    ├── Element (head)
    └── Element (body)
        ├── Text
        └── Element (div)  ── shadowRoot ──► ShadowRoot
                                             └── (its own tree)

The DOM is not one tree. It is a node tree, plus shadow trees attached to hosts, and the composed/flat tree that results from slotting. Style and layout operate on the flat tree, not the node tree. Almost every confusing shadow-DOM behaviour — inheritance across boundaries, ::slotted, event retargeting — is a consequence of those being different trees.

If you take one thing from this module: "the DOM tree" is ambiguous, and the ambiguity is where the bugs are.

Attributes are not properties

element.setAttribute("class", "foo") and element.className = "foo" reach the same place, but the model is: attributes are the serialised, spec-defined store; IDL attributes ("properties") are a reflected view with type coercion. Some reflect, some don't (input.value is the classic divergence — attribute is the default, property is the current value). Frameworks that set properties and frameworks that set attributes behave differently on exactly these, which is a recurring source of "works in React, breaks in Vue"-style reports.

Two garbage collectors

This is the part that surprises people, and it is the key to lifetime reasoning.

  • V8's GC manages JavaScript objects, including the JS wrapper for a DOM node.
  • Oilpan (Blink's GC, in platform/heap/) manages the C++ DOM objects themselves.

A <div> therefore has two objects: a C++ HTMLDivElement on Oilpan's heap, and a V8 wrapper on V8's heap. Neither collector can independently decide the pair is dead — if JS holds the wrapper, the C++ object must live; if the C++ object is reachable from the document, the wrapper must live so that expando properties survive a round trip:

const d = document.createElement('div');
d.myExpando = 42;
document.body.appendChild(d);
// ... later, with no JS reference retained
document.body.firstChild.myExpando  // must still be 42

That requirement — wrapper identity and expando survival — is why the two heaps must be traced together rather than separately. It is a genuinely hard problem, and it is the reason the bindings layer (bi-05) is as complicated as it is.

Terminology, verified 2026-08-10. The mechanism that used to do this was called wrapper tracing, annotated with TraceWrapperMember<T>. That is deprecated and the type no longer exists. Blink now uses V8's unified heap: Oilpan is built on cppgc, which lives in V8, so one collector traces C++ and JavaScript objects together. In current code you use plain Member<T> for managed pointers regardless of whether a JS object is transitively reachable, and TraceWrapperV8Reference<T> for references into V8 that this object must keep alive. A blog post about TraceWrapperMember is describing a browser that no longer exists.

Oilpan's handle vocabulary

You cannot read core/dom/ without these. From platform/heap/BlinkGCAPIReference.md:

TypeMeaning
GarbageCollected<T>base class: this type lives on Oilpan's heap
Member<T>strong reference from one GC object to another
WeakMember<T>weak; cleared automatically when the target dies
UntracedMember<T>not traced — you are asserting lifetime some other way
Persistent<T>strong root from non-GC code into the heap
WeakPersistent<T>weak root
CrossThreadPersistent<T>root usable from another thread
STACK_ALLOCATED()this class may only exist on the stack
DISALLOW_NEW()may only exist inline in another object
Trace(Visitor*)how an object declares its outgoing edges

Reading rules that pay off immediately:

  • Member<T> in a class means "part of the object graph." The Trace method is the authoritative list of what an object keeps alive — read it first when reasoning about leaks. It is the ownership documentation, and unlike a comment it cannot silently rot.
  • Persistent<T> held by something long-lived is the classic leak shape. A root that is never dropped keeps an arbitrary subgraph alive.
  • STACK_ALLOCATED() is a strong hint about intended lifetime, enforced by a clang plugin. If you find yourself wanting to store one, your design is fighting the framework.

Blink also constrains ordinary types: String/AtomicString and WTF containers rather than STL, KURL not GURL, SecurityOrigin not url::Origin. This is enforced by DEPS, by audit_non_blink_usage.py, and by a clang plugin. When a codebase enforces a convention with three separate mechanisms, that is a signal about how often people got it wrong — and about how seriously reviewers will take it in your first CL.

Mutation → invalidation

The step that connects this module to the rest of the track:

DOM mutation
  → node marked dirty (style invalidation: "which elements might need recalculation?")
  → ancestors/descendants marked as needing work, per invalidation sets
  → at the next rendering opportunity: style recalc → layout → paint

The essential insight is that mutation does not recompute anything. It records that something must be recomputed later. This is why a thousand DOM writes in a loop are not a thousand layouts — and why one interleaved read of a geometry property forces a synchronous flush and turns the loop quadratic. That is the forced-synchronous-layout bug, and it is Vertical Trace #2.


Under the Hood

Re-derive rather than memorise (bi-01 Techniques 1–4). Places to be able to find:

ConcernWhat to look for
Node/Element/Document base classescore/dom/
Child list manipulation and its checksthe container-node insertion/removal paths
Attribute storageelement attribute collections; note the space optimisation for few attributes
Shadow trees, slotting, flat-tree traversalshadow root + flat tree traversal helpers
Mutation observersthe mutation observer registration/queueing machinery
Custom element reactionsthe reaction stack/queue
LifetimeTrace() methods, platform/heap/

Two questions worth answering by reading, because they teach the design:

  1. Why does appendChild do so much work before mutating? (Hierarchy checks, adoption into the right document, removal from the old parent, ordering guarantees around observers and custom element reactions.) The naive "set a pointer" model is wrong in about six ways, each of which is a spec requirement.
  2. Why are custom element reactions queued rather than run immediately? Because running author code in the middle of a DOM mutation would let it observe — and mutate — a tree in an inconsistent intermediate state. This is the same reason bi-03's construction site queues tasks. Notice that you have now met the same design pressure twice. That pattern-recognition is the actual skill.

Deep dive: Oilpan, as actually configured

From platform/heap/BlinkGCAPIReference.md, with the general design living in V8's cppgc README — that inheritance is the whole story: Blink's GC is V8's C++ GC with Blink-specific extensions, which is exactly what makes one unified collector over both heaps possible.

Threading

"Oilpan assumes heaps are not shared among threads."

Threads that allocate Oilpan objects must be attached (ThreadState::AttachMainThread() / AttachCurrentThread()). Blink creates heaps and root sets per thread, and an object belongs to the thread it was allocated on.

This is the enforcement behind renderer/README.md's rule that cross-thread communication is message passing, not shared memory. It is not a style preference — the heap is structured so that sharing objects across threads is the exceptional path, requiring explicit CrossThreadPersistent / CrossThreadHandle types that announce themselves in the code.

Heap partitioning — and what the partitions disclose

Blink assigns certain types to custom spaces:

  • collection backings to compactable custom spaces (so vectors and hash tables can be moved and the heap defragmented),
  • Node, CSSValue, and LayoutObject to typed custom spaces.

Read that second line as a performance disclosure. Those three types get dedicated spaces because they are the highest-volume allocations in the renderer. A page is, in allocation terms, mostly nodes, CSS values, and layout objects — a useful corrective to any mental model where "the DOM" is the only thing that costs memory. Your stylesheet allocates too, and CSSValue earning its own heap space is the evidence.

Mode of operation

  • Concurrent marking and sweeping (except during thread termination and heap destruction).
  • GCs are scheduled through the message loop, at points where no objects are referenced from the native stack — so the collector can be precise rather than conservative.
  • Under memory pressure, Blink triggers a conservative GC on allocation.

The precise/conservative distinction is worth holding. A precise GC knows exactly which words are pointers; a conservative one must treat anything that looks like a pointer as one, and therefore retains garbage. Blink prefers precise collection and schedules for it — the message loop is not only a scheduler, it is a GC safepoint mechanism. That is a genuinely non-obvious coupling between bi-11 and this module, and it is the kind of cross-subsystem constraint that never appears in documentation about either one alone.

ActiveScriptWrappable: reachability is not the whole story

/**
 * Classes deriving from ActiveScriptWrappable will be kept alive as long as
 * they have a pending activity. Destroying the corresponding ExecutionContext
 * implicitly releases them to avoid leaks.
 */

This solves a problem pure reachability cannot:

new XMLHttpRequest().addEventListener('load', () => console.log('done'));
// nothing holds a reference to the XHR

Reachability says garbage. Correctness says it must survive until the request completes and fires its event. ActiveScriptWrappable is the mechanism — "I have pending activity, keep me alive." The same applies to a playing <audio>, an open WebSocket, a pending animation.

Two transferable lessons:

  1. "Unreachable" and "collectable" are not synonyms in a system with external observable effects. Any framework with subscriptions has this problem in miniature — which is why fw-02's effect-cleanup lab matters.
  2. The ExecutionContext teardown clause is the leak guard: when the document goes away, pending activity is released regardless. Without it a navigated-away page could pin memory forever. Every "keep alive" mechanism needs a matching "and here is when we stop."

Deep dive: wrappers are lazy, and what that costs

A DOM node has no JavaScript object until JavaScript observes it. Wrappers are created on demand.

This is the detail most people miss, and it changes the cost model:

  • A 50,000-node document that script never touches costs far less on the V8 side than one where a framework has walked the whole tree.
  • A gratuitous document.querySelectorAll('*') does not merely cost the query — it materialises wrappers for everything it returns, permanently (until GC), on both heaps.
  • Frameworks that hold references to every DOM node they created keep every wrapper alive.

Rules of thumb (anchors, not measurements — verify on your own pages):

QuantityRough scale
Per-element C++ costhundreds of bytes to low kB, depending on what is attached
Per-element JS wrapperzero until touched, then a real object on V8's heap
Style recalcproportional to invalidated elements, not total (bi-07)
Layoutproportional to dirty boxes plus intrinsic-sizing pre-passes (bi-08)

Deep dive: the mutation-observation family, ordered

Several mechanisms observe DOM change and they do not run at the same time. Getting the order right explains a large class of "why did my callback see stale state" bugs.

MechanismWhen it runs
Custom element reactionsreaction queue, drained at defined checkpoints during the mutating algorithm
MutationObservermicrotask checkpoint after the current task
ResizeObserverinside update the rendering, after layout — and may loop
IntersectionObserverinside update-the-rendering, delivered asynchronously
mutation events (legacy)synchronously, mid-mutation — which is why they were deprecated

The historical arc is the lesson. Mutation events fired synchronously in the middle of DOM algorithms, so author code could observe and mutate a tree in an inconsistent intermediate state. They were catastrophic for correctness and performance alike, and MutationObserver replaced them with batched, microtask-delivered records.

You have now met the same design pressure three times: the parser's construction site queues tasks (bi-03), custom element reactions are queued, and mutation observation is batched. Each time the requirement is identical — author code must never observe a half-built tree. When you design any API that notifies on change, this is the first question to ask.

ResizeObserver is the instructive outlier: it deliberately runs inside the rendering steps and is permitted to loop, with a depth limit and an error when exceeded, because resize-driven layout changes genuinely need to reach a fixed point before paint. Sometimes the correct answer is a bounded loop, not a ban — and the bound is what makes it shippable.


Deep dive: the trees, precisely

"The DOM tree" is ambiguous. Name which one you mean:

TreeWhat it isWho consumes it
Node treeas authored; parentNode / childNodesDOM APIs
Shadow treea ShadowRoot and its descendantsencapsulation
Flat treenode tree with shadow content slotted instyle and layout
Layout treeboxes; no display:none; anonymous boxes addedlayout (bi-08)
Fragment treeimmutable geometry resultspaint (bi-09)
Accessibility treesemantic representationassistive tech, and testing (fw-12)

Behaviours that make no sense without the flat tree:

  • Inheritance crosses shadow boundaries — inherited properties flow host to shadow content, because inheritance follows the flat tree.
  • ::slotted() and ::part() exist because ordinary selectors cannot cross the boundary, even though the flat tree has already merged the content.
  • Event retargeting — a listener outside a shadow root sees the host as target, not the internal element. composedPath() reveals the real path.

And the fact that surprises people most: slot assignment does not move nodes. parentNode is unchanged; only the flat tree differs. Any code reasoning about layout from parentNode is reasoning about the wrong tree.


Deep dive: attributes, properties, and the reflection table

CaseAttributePropertyRelationship
id, classid, classid, classNamereflected both ways
<input value>the default valuethe current valuediverges after user input
<input checked>default checkednesscurrent checkednesssame divergence
href on <a>as authoredresolved absolute URLproperty is not the attribute
data-*attributedatasetlive view
styleserialised textCSSStyleDeclarationan object, not a string

The href row is the classic gotcha: el.getAttribute('href') and el.href differ for a relative URL, because the property resolves against the document base.

Why frameworks disagree here. A framework that sets properties and one that sets attributes behave differently on exactly the diverging rows. "It works in React but not in my web component" is very often this: React setting a property where the component expected an attribute change and an attributeChangedCallback that consequently never fired.


Anti-Patterns

"The DOM is slow." DOM writes are cheap; they mark dirty. What is expensive is forcing a synchronous recomputation, and creating enormous trees. A model that says "DOM = slow" cannot predict which of two loops is 100× worse.

Reasoning about shadow DOM using the node tree. Style and layout use the flat tree.

Assuming property ↔ attribute equivalence. Especially for form controls.

Treating MutationObserver as free. It queues microtasks; a chatty observer on a hot subtree can dominate a frame.

Using detached DOM nodes as a cache. A detached subtree held by a JS variable keeps the whole subtree — and everything its listeners close over — alive across both heaps.


Trade-offs

Two GCs vs one. Unifying them would simplify lifetime enormously. It is not done because V8 is a separate project with its own release cadence, embedders other than Blink, and its own performance constraints. The cost of the split is the entire wrapper-tracing mechanism — a large, permanent complexity budget spent on an organisational boundary as much as a technical one. Notice that: some production complexity exists because of who owns what, not because of what the machine needs. That is a §46 entry worth writing carefully.

Queued reactions vs immediate callbacks. Queuing preserves tree consistency but makes ordering subtle and hard to reason about. Immediate callbacks would be simpler to explain and impossible to make safe.

Attribute storage compactness vs access speed. Elements overwhelmingly have few attributes, so Blink optimises for the small case. Find the actual data structure and decide whether you would have made the same call.


Lab

mini-browser M3 — a real DOM.

  1. Node, Element, Text, Document; parent/child/sibling links.
  2. appendChild, insertBefore, removeChild, remove with correct ordering and hierarchy checks (including: appending a node that already has a parent removes it first; appending an ancestor to its own descendant must throw).
  3. Attributes with a separate reflected-property layer for at least class and id.
  4. A dirty-marking scheme: mutations mark nodes, and nothing is recomputed until a flush().
  5. MutationObserver-alike with microtask-queued delivery.
  6. A deliberate second implementation of dirty-marking: naive (recompute everything) vs scoped. Measure both on a 10,000-node tree.

Deliverable: a table showing operations/second for both schemes, and a written explanation of where the crossover is and why.


Failure Lab

  1. Forced synchronous layout. Write a loop that writes then reads geometry each iteration. Measure. Then batch reads and writes. Explain the complexity change, not just the wall clock.
  2. The detached-subtree leak. Build one deliberately. Find it in DevTools' heap snapshot. Then find the retaining path — the skill is reading the retainer chain, not noticing the growth.
  3. Observer storm. Attach a MutationObserver that mutates the DOM in response to mutations. Predict the outcome before running: infinite loop, or something subtler? Explain in terms of the microtask checkpoint.
  4. Property/attribute divergence. Build a form where setting the attribute after user input does nothing visible. Explain to a hypothetical junior engineer in three sentences.

Debugging Exercise

  1. Breakpoint on the container-node insertion path. From document.body.appendChild(el) in the console, capture the full stack: how many frames between the binding and the actual pointer update?
  2. Find the Trace() method of an Element. List what it keeps alive. Predict what happens to a listener closure when the element is removed but a JS reference is retained — then verify with a heap snapshot.
  3. Use tracing to observe that a batch of DOM mutations produces exactly one style recalc. Then insert a geometry read into the loop and observe the change.

Testing & QA Considerations

  • Find the WPT for appendChild hierarchy-check errors. What exception types are specified, and does Blink match on every branch?
  • Find a core/dom/ unit test that asserts an ordering property (observers vs custom element reactions). What would break if the order changed?
  • Write a test that would catch a regression in the property/attribute reflection of one form control.

Core terminology

TermDefinition
Node treeThe literal parent/child structure of the document
Shadow treeAn encapsulated subtree attached to a host element
Flat / composed treeThe result of slotting; what style and layout actually operate on
SlottingDistributing light-DOM children into <slot> elements in a shadow tree
Event retargetingAdjusting event.target at shadow boundaries to preserve encapsulation
Light DOMThe host's ordinary children, as authored
OilpanBlink's garbage collector for C++ DOM objects; built on cppgc, which lives in V8
Unified heapV8 + Oilpan traced together; the modern mechanism replacing wrapper tracing
WrapperThe V8 object representing a C++ DOM node to JavaScript
Wrapper identityThe requirement that the same node yields the same JS object every time
ExpandoA JS-added property on a wrapper; must survive round trips, which forces cross-heap tracing
Member<T> / WeakMember<T> / Persistent<T>Oilpan handles — not interchangeable with //base pointer types
AttributeThe serialised, spec-defined store, addressed by getAttribute/setAttribute
IDL attribute (property)The reflected view with type coercion
ReflectionThe mapping between the two; incomplete by design
MutationObserverBatched, microtask-delivered DOM change notification
Live collectionHTMLCollection/NodeList that reflects the tree as it changes; a classic cost trap
Detached subtreeRemoved from the document but still referenced; retains its entire subtree

Mental models

Always say which tree. Node, shadow, or flat. Style and layout use the flat tree; event paths use the composed path with retargeting; querySelector uses the node tree of the scope it is called on. Most shadow-DOM confusion dissolves the moment the question names a tree.

Two collectors, one lifetime. A DOM node exists on both heaps and the pair lives or dies together. This is the C++ side of fe-02's "the heap is not one heap" — and it is why a JS-only memory instrument reports a nearly flat line across a large DOM leak.

Attributes are the serialisation; properties are the view. When a framework and the platform disagree about a control's state, the first question is whether the framework wrote the attribute or the property, and whether that pair reflects.

Delivery timing is what distinguishes the observation APIs. MutationObserver delivers on a microtask — the earliest point at which the current synchronous work is finished — while ResizeObserver and IntersectionObserver deliver inside the rendering steps. Choosing between them is choosing when you want to see the change, not what you want to see.

A detached subtree retains its whole subtree. One held reference to a leaf keeps every ancestor alive, with their listeners and data. This is fe-02's detached-DOM shape, explained from the C++ side.

Live collections are a cost model, not a convenience. A getElementsByTagName result that updates as the tree changes has to be kept consistent, and iterating one while mutating is a classic quadratic trap.


Common misconceptions

  1. "The DOM is a tree." It is several. Naming which one is the first step in any shadow-DOM, style, or event question.

  2. "Style operates on the DOM tree." It operates on the flat tree. That is why slotted content inherits from its slot's ancestors rather than its authored parent.

  3. "element.value and the value attribute are the same." The attribute is the default; the property is the current value. They diverge as soon as the user types.

  4. "Removing a node from the document frees it." Only if nothing else references it — and a reference to a leaf retains the whole detached subtree.

  5. "performance.memory will show a DOM leak." DOM nodes are on the C++ heap. This is the same finding fe-02 measures from outside.

  6. "Oilpan handles and //base smart pointers are interchangeable." Mixing the two vocabularies is the most common newcomer error in Blink.

  7. "MutationObserver fires immediately." It batches and delivers on a microtask, which is why you observe a set of changes rather than each one.

  8. "Shadow DOM encapsulation means events are contained." They propagate, with retargeting — which preserves encapsulation without stopping the event.


Interview talking points

  • "'The DOM tree' is ambiguous, and the ambiguity is where the bugs are. There's a node tree, shadow trees, and the flat tree that slotting produces — and style and layout operate on the flat one. Nearly every confusing shadow-DOM behaviour is a consequence of those being different trees."
  • "A <div> is two objects: a C++ node on Oilpan's heap and a V8 wrapper. Neither collector can independently decide the pair is dead, because expandos have to survive a round trip through the document. That requirement is why the two heaps are traced together and why the bindings layer is as complicated as it is."
  • "Attributes are the serialised store; IDL properties are a reflected view with coercion, and the reflection is deliberately incomplete. input.value is the classic case — attribute is the default, property is the current value — and it's behind a lot of 'works in React, breaks in Vue' reports."
  • "I choose between observation APIs on delivery timing, not capability. MutationObserver delivers on a microtask; ResizeObserver and IntersectionObserver deliver inside the rendering steps, after layout, which is what makes them safe to measure in."
  • "A detached subtree retains its entire subtree — one reference to a leaf keeps every ancestor and their listeners alive. That's the C++ explanation for why a JS-heap graph stays flat across a large DOM leak."

Further Reading (primary sources first)

  • DOM Standard (WHATWG) — node tree, mutation algorithms, MutationObserver.
  • HTML Standard — reflection of IDL attributes to content attributes; form control state.
  • DOM Parts / shadow DOM sections for the flat tree and slotting.
  • third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md — the Oilpan reference. Read "Handles" and "Tracing" properly; skim the rest.
  • third_party/blink/renderer/README.md §"Type dependencies".
  • third_party/blink/renderer/core/dom/ — and its Trace() methods specifically.

Principal Engineer Review

  1. "The DOM is slow" — replace this with a model precise enough to predict which of two implementations is faster. Give a case where the folk model predicts the wrong answer.

  2. Explain wrapper tracing to a senior engineer who knows JS GC but not Blink. Why can't the two collectors run independently? What breaks first if they do?

  3. A long-lived SPA grows 40 MB per hour. Enumerate causes in order of likelihood, and give the fastest discriminating observation for each.

  4. Style and layout use the flat tree, not the node tree. Give two developer-visible behaviours that make no sense without this fact.

  5. Custom element reactions and mutation observers are both queued rather than immediate. Reconstruct the requirement that forces this. What would a synchronous design break — specifically?

  6. Argue for merging V8's heap and Oilpan. What would improve, what would become impossible, and what non-technical constraint is doing most of the work in the actual decision?

  7. A framework claims it "batches DOM updates for performance." Given this module, what is it actually saving — and in what situation does it save nothing at all?

  8. Design a DOM API that makes forced synchronous layout impossible to express. What do you lose? Would you ship it as a replacement, or alongside?

  9. Blink enforces its type conventions with DEPS, a presubmit script, and a clang plugin. What does three-mechanism enforcement tell you, and when is that level of enforcement worth it on a team you lead?

  10. You must explain to a product manager why a "simple" DOM change caused a 30 % regression in a rendering benchmark. Do it in five sentences, without using the words "reflow" or "repaint."


Connections to other modules

  • bi-03 — what the parser hands you, and the trees it builds into.
  • bi-05 — the bindings layer whose complexity this module's wrapper requirement explains.
  • bi-07 — style operates on the flat tree; invalidation is defined over it.
  • bi-09 — hit testing and event dispatch use the composed path.
  • fe-02 — the same two-heap picture from outside, with the measurement showing a JS-heap graph flat across a 508 KB DOM leak.
  • fe-04 — the accessibility tree is a fourth derived tree, and Shadow DOM changes focus and AT traversal.
  • fw-02 — the DOM cost model that makes the virtual-DOM bet worth taking.
  • fw-10 — DOM size as the constraint virtualization exists to manage.