bi-05 — The Binding Layer: Web IDL, V8, and the JS↔Blink Boundary

Phase 1 · Spec areas §9 (binding layer), §17 (V8 integration). Prerequisites: bi-01, bi-02, bi-04.

Cross-track hook: frontend-principal-engineering.md §1 (JS runtime). That module covers what JS guarantees; this one covers what happens when JS calls something that is not JS.


0. Orientation

The 60-second version

The bindings layer is the seam between JavaScript and C++, and it is where four separate costs hide behind syntax that looks like a field read.

document.title      // looks like a property access. It is a cross-language call.
  1. Crossing — the call is not inlineable into V8's optimised code the way a plain JS property is.
  2. Conversion — every string, number, dictionary and sequence is translated between representations. Strings are the expensive one.
  3. Checks — Web IDL type coercion, plus security checks on cross-realm objects.
  4. Side effects — some getters flush style and layout.

Cost 4 is the one that produces order-of-magnitude surprises, and it is invisible in any JS-level mental model. offsetWidth is not a field read; it is a command to make layout current. That is fe-01's 98× layout-thrashing result, explained from the inside.

Web IDL is the contract, and it is executable

Every web-exposed interface is declared in .idl, and the IDL compiler turns it into the C++ V8 calls. So the .idl file is the authoritative statement of a Web API's surface — argument types, nullability, whether it throws — and extended attributes are where the semantics hide:

Extended attributeEffect
[CEReactions]wraps the operation in the custom-element reaction stack
[CrossOrigin]accessible across origins — a deliberate hole in the wall
[SameObject]must return the identical object every time (identity, not equality)
[Replaceable]assignment shadows rather than calls a setter
[RuntimeEnabled=Foo]exists only when the feature flag is on
[Unforgeable]non-configurable — page script cannot shadow it

[Unforgeable] is a security primitive: it is why a page cannot redefine window.location to fool code that reads it. When you see it, you are looking at a decision someone made because an attacker would otherwise have a hole.

What you should be able to do after this module

CapabilityEvidence you have it
Price a DOM API callSeparate crossing, conversion, checks and side effects
Read an .idl fileTreat it as the authoritative API surface, extended attributes included
Explain a forced layoutSay why a getter is a command rather than a read
Reason about isolationExplain worlds, and why an extension's JS cannot see page expandos

Why a Principal Engineer needs this

1. It is where "JavaScript performance" stops being about JavaScript. A tight loop over element.style.width is not slow because of V8. It is slow because every property access crosses a boundary that does type conversion, security checks, and possibly a style flush. Engineers who profile only in the JS flame chart cannot see this and conclude the wrong thing.

2. Every Web API's shape is decided here, and the shape has consequences. Whether something is a getter or a method, whether it returns a live collection or a static list, whether it throws or returns null — these are IDL decisions with real performance and correctness implications. document.querySelectorAll returning a static NodeList while getElementsByTagName returns a live HTMLCollection is an IDL fact that has caused an enormous number of production bugs.

3. It is the security boundary inside the renderer. Cross-origin access checks on window and location are enforced in the bindings. Understanding this is what lets you reason correctly about what an iframe can and cannot touch.

4. It is the largest generated-code surface in the tree, and therefore the place where bi-01 Technique 4 earns its keep. bindings/generated_in_modules.gni — the listing of generated files — is over 300 KB by itself. If you try to navigate this subsystem by grepping for hand-written C++, you will conclude that most of the Web Platform does not exist.


Mental Model

The path of one property access

document.title
V8 executes bytecode; sees a property load on a JS object
  ↓  the object is a "wrapper" with an interceptor / accessor installed
GENERATED binding code (from document.idl)
  ↓  unwrap: JS object → C++ blink::Document*
  ↓  security check (is this realm allowed to touch this object?)
Blink C++: Document::title()
  ↓  returns WTF::String
GENERATED binding code
  ↓  convert WTF::String → v8::String (may allocate, may copy)
back to JS

Four costs live in that diagram, and naming them separately is the whole point:

  1. Crossing — the call itself is not inlineable into V8's optimised code the way a plain JS property is.
  2. Conversion — every string, number, dictionary and sequence is translated between representations. Strings are the expensive one.
  3. Checks — type coercion per Web IDL rules, plus security checks on cross-realm objects.
  4. Side effects — some getters flush style/layout. offsetWidth is not a field read; it is a command to make layout current.

Cost 4 is the one that produces order-of-magnitude surprises, and it is invisible in any JS-level mental model.

Web IDL is the contract, and it is executable

Every web-exposed interface is declared in .idl. The IDL compiler turns it into C++ that V8 calls. This means:

  • The .idl file is the authoritative statement of a Web API's surface. Argument types, nullability, whether something throws, whether it is [Replaceable], [SameObject], [CEReactions] — all declared, not implemented ad hoc.
  • Extended attributes are where the semantics hide. Blink documents these in bindings/IDLExtendedAttributes.md (~69 KB — that size is itself informative). A few that change behaviour dramatically:
Extended attributeEffect
[CEReactions]wrap the operation in the custom-element reaction stack
[CrossOrigin]accessible across origins — a deliberate hole in the wall
[SameObject]must return the identical object every time (identity, not equality)
[Replaceable]assignment shadows rather than calls a setter
[RuntimeEnabled=Foo]exists only when the feature flag is on
[Unforgeable] / [LegacyUnforgeable]non-configurable — cannot be shadowed by page script

[Unforgeable] is a security primitive: it is why a page cannot redefine window.location to fool code that reads it. When you see it, you are looking at a decision someone made because an attacker would otherwise have a hole.

Wrappers, identity and expandos

Each C++ DOM object has at most one JS wrapper per world, and that wrapper must be stable:

document.body === document.body   // true — same wrapper object
el.myExpando = 1;                 // stored on the wrapper

If the wrapper were recreated on each access, expandos would vanish and identity comparisons would fail. So the binding layer maintains a wrapper map and keeps the pair alive together (bi-04's two-GC problem). Wrapper identity is the requirement that forces most of the complexity in this layer — it is the answer to "why not just create a fresh object each time."

Worlds and realms

  • A realm/context is roughly one global object — one window.
  • A world is Blink's separation between the page's JS and isolated worlds, notably extension content scripts. The same C++ DOM node has a different wrapper per world, so expandos set by an extension are invisible to the page. This is a security feature and it is why "just use a content script" is a real isolation boundary rather than a convention.

Cross-origin window/location access goes through remote-object proxies with per-property checks — only the [CrossOrigin] members get through. This is the mechanism behind every "blocked a frame with origin ... from accessing a cross-origin frame" message.

V8, precisely bounded

You need only enough V8 to reason about the boundary. Know these and stop:

  • Isolate — one instance of the engine, one heap. One per thread; a worker has its own.
  • Context — one global object within an isolate.
  • Hidden classes / shapes — V8's representation of object layout. Monomorphic call sites are fast; polymorphic ones degrade. This is why consistent object shapes matter.
  • Inline caches — per-call-site memoisation of "what shape did I see here." Bindings interact with these: a DOM accessor is not a plain property, so it optimises differently.
  • Deoptimisation — the optimising compiler bails back to bytecode when an assumption breaks.
  • The GC boundary — V8's heap vs Oilpan's heap (bi-04).

The distinction you must never blur: V8 executes JavaScript. Blink implements the Web Platform. document is not a JavaScript concept — it is a Blink object with a JS wrapper. Node.js has V8 and no document; that is the cleanest demonstration of the split.


Under the Hood

Navigation targets (derive them; do not memorise):

ConcernWhere to look
IDL definitions.idl files throughout core/ and modules/
The IDL compilerbindings/scripts/, documented in bindings/IDLCompiler.md
Extended attribute semanticsbindings/IDLExtendedAttributes.md
Generated output listingbindings/generated_in_core.gni, generated_in_modules.gni
Actual generated C++out/Default/gen/third_party/blink/renderer/bindings/
Wrapper base classthe script-wrappable type in platform/bindings/
Wrapper tracing / GC integrationplatform/bindings/ + platform/heap/

Do this once and it will pay for itself: open a generated binding for a simple interface and read it end to end. You will see the unwrap, the security check, the argument conversion, the exception handling, and the return conversion, all explicit. After that, every performance claim about "DOM access is slow" becomes concrete rather than folkloric.

Deriving the mapping rules

Given partial interface Document { attribute DOMString title; }, you should be able to predict the generated C++ names and find Document::title() / Document::setTitle(). The mangling is mechanical (IDL camelCase → C++ PascalCase, attributes → getter/setter pairs). Once you know the rule, you can jump from any Web API name to its implementation in one step — which is bi-01 Technique 4 applied at its highest leverage.


Deep dive: V8's execution tiers, bounded to what you need

Verified present in the checkout (v8/src/): interpreter/, baseline/, maglev/, compiler/. That is four tiers, and knowing their names and trade-offs is the right amount of V8 for a browser engineer.

TierDirectoryWhat it isTrade
Ignitionsrc/interpreterbytecode interpreterstarts instantly, runs slowest
Sparkplugsrc/baselinenon-optimising baseline compilernear-instant compile, modest speedup
Maglevsrc/maglevmid-tier optimising compilerfast compile, good code
TurboFansrc/compilertop-tier optimising compilerslow compile, best code

Code moves up tiers as it gets hot, and falls back down on deoptimisation when an assumption the optimiser relied on turns out false.

Why four tiers rather than two

This is a scheduling problem wearing a compiler costume, and the shape should be familiar by now. Compiling is work that competes with running. A single optimising compiler is either too eager (you pay compile time for code that runs twice) or too lazy (hot code stays slow). Tiering is the same "spend a little to decide whether to spend a lot" pattern as tile prioritisation (bi-10) and speculative parsing (bi-03).

What this means for you as a web engineer: almost nothing directly, and that is the point. You cannot control tier-up. What you can control is whether your code is optimisable at all — which is about object shapes.

Hidden classes and inline caches, precisely enough

V8 does not store JS objects as hash maps when it can avoid it. Objects with the same shape (same properties, added in the same order) share a hidden class (a "map"), so a property access compiles to an offset load rather than a lookup.

A property-access site records the shapes it has seen — that is the inline cache:

Site stateShapes seenSpeed
monomorphic1fastest — direct offset
polymorphica few (~2–4)a small check chain
megamorphicmanyfalls back to a generic lookup

Consequences that are actually actionable:

  • Initialise all fields in the constructor, in a consistent order. Adding a property later creates a new shape and transitions the object.
  • delete obj.x is expensive — it can force the object into dictionary mode, losing the shape entirely.
  • Passing objects of many different shapes to one hot function makes its access sites megamorphic.

But the honest framing matters more than the tips:

This almost never matters in application code. It matters in library and framework internals — a reconciler's hot loop, a reactivity system's dependency map. Optimising object shapes in product code is usually a misuse of your time. Knowing the mechanism is for the day you are reading fw-11's production source and wondering why React's Fiber objects initialise every field to null in the constructor. That is why.

Deoptimisation

TurboFan compiles on assumptions ("this is always a small integer," "this object always has this shape"). When an assumption breaks, the code deoptimises: execution transfers back to the interpreter mid-function. Deopt loops — optimise, deopt, re-optimise — are a real pathology, and the reason a function can be mysteriously slow only in production data.

You will not usually debug this. You should recognise the name when it appears in a trace.


Deep dive: the unified heap, and why this layer exists

bi-04 established the two-heap problem. Here is the machinery, current as of 2026-08-10.

Oilpan is built on cppgc, which lives in V8 (v8/src/heap/cppgc-internal/, cppgc-js/). That location is the whole design: because the C++ collector is part of V8, one collector can trace both heaps and correctly collect cycles that span them.

JS object  ──references──►  C++ DOM object  ──references──►  JS callback
     ▲                                                            │
     └────────────────────────────────────────────────────────────┘
                      a cycle across two heaps

Two independent collectors can never collect that cycle: each sees an external root. A unified collector can. This is not an optimisation — it is the difference between "we leak every DOM node with an event listener" and not.

The historical mechanism was wrapper tracing with TraceWrapperMember<T>; it is deprecated and the type is gone. Today:

  • Member<T> for managed pointers, regardless of JS reachability,
  • TraceWrapperV8Reference<T> for references into V8 that this object must keep alive.

Verification lesson. This is the second time in this track a widely-cited mechanism turned out to be retired (ng_ prefixes were the first). Both would have been repeated confidently from memory. Both took one minute to check in the tree. That ratio is why bi-01's navigation discipline is the first module.


Deep dive: worlds, and the isolation you can observe

A world is Blink's separation between the page's JavaScript and isolated worlds — notably extension content scripts. DOMWrapperWorld is the type.

The same C++ DOM node has a different wrapper per world:

// page script
document.body.pageOnly = 1;

// extension content script, same document, different world
document.body.pageOnly    // undefined

Both see the same <body>; neither sees the other's expandos. Prototype pollution in one world does not reach the other.

This is a real security boundary, not a convention, and it explains several things at once:

  • why a content script cannot be trivially detected by page script reading its expandos,
  • why extensions must use postMessage or window.wrappedJSObject-style bridges to interact,
  • why "just inject a script tag" behaves differently from a content script — injected script runs in the main world.

Realms, and cross-origin access

A realm/context is roughly one global object. Cross-origin window and location access goes through remote-object proxies that permit only the [CrossOrigin]-marked members. Everything else throws. That is the mechanism behind:

Blocked a frame with origin "https://a.example" from accessing a cross-origin frame.

The allow-list is small and deliberate: window.postMessage, window.location.href (write-only for cross-origin), window.closed, window.frames, window.top, and a handful more. Every entry is a decision someone defended.


Deep dive: extended attributes that change behaviour

bindings/IDLExtendedAttributes.md runs to roughly 69 KB. That size is informative: it is two decades of accumulated requirements. The ones worth recognising on sight:

AttributeEffectWhy you care
[CEReactions]wraps the operation in the custom-element reaction stackordering guarantees (bi-04)
[Unforgeable] / [LegacyUnforgeable]non-configurable, non-shadowablesecurity primitive
[CrossOrigin]reachable across originsa deliberate hole in the wall
[SameObject]must return the identical object each timeidentity, not equality
[Replaceable]assignment shadows instead of calling a setterlegacy compatibility
[RuntimeEnabled=X]exists only when the flag is on(bi-01 Technique 4)
[SecureContext]only in secure contextsHTTPS-gated APIs
[Exposed=Window,Worker]which global(s) expose itwhy an API is missing in a worker
[PutForwards]assignment forwards to a member of the returned objecte.g. location = url
[LegacyLenientThis]tolerate wrong this instead of throwingpure compatibility

[Exposed=...] is the one that saves application engineers the most time. "Why is document undefined in my worker?" is answered by an IDL annotation, not by a bug.

[Unforgeable] on window.location is worth internalising as a security pattern: a page cannot redefine it, so code that reads location to make a trust decision cannot be fooled by script that ran earlier. When you design an API whose value is used for a security decision, ask whether an attacker can shadow it.


Deep dive: what a generated binding actually does

Read one once and this whole layer stops being mysterious. Every generated accessor performs roughly the same sequence:

1. Unwrap:      v8::Object  →  blink::C++ object      (type check; throw TypeError if wrong)
2. Check:       is this realm/world allowed to touch this object?   (cross-origin)
3. Convert:     JS values → IDL types, per Web IDL conversion algorithms
                (this is where "3" becomes 3, where undefined becomes "undefined", etc.)
4. Call:        the Blink implementation method
5. Convert:     the result back to a V8 value (may allocate — strings especially)
6. Exceptions:  translate Blink exceptions into JS exceptions on the right realm

Two things become obvious once you have seen this written out:

  • Type coercion is specified, not incidental. Web IDL defines exactly how a JS value becomes a long, a DOMString, a dictionary. The surprising coercions in DOM APIs are conformance, not sloppiness.
  • Strings cost. Every DOMString crossing the boundary is a conversion between V8's representation and WTF::String, potentially with allocation. In a hot loop reading el.className, that conversion is the cost.

The size of the generated surface

bindings/generated_in_core.gni is ~197 KB and generated_in_modules.gni ~338 KB — and those are merely the lists of generated files, not the files themselves. Half a megabyte of filenames.

Sit with that number for a moment. It is the most direct evidence available that bi-01 Technique 4 is not a curiosity: the majority of the Web Platform's C++ surface is not written by hand. Any navigation strategy that assumes otherwise fails on most of the platform.


Anti-Patterns

Profiling only the JS flame chart. Binding cost, style flushes triggered by getters, and GC across two heaps do not appear as "your function."

Assuming DOM property access is a field read. Some are. offsetWidth, getComputedStyle reads, and getBoundingClientRect are commands that may force layout.

Caching a live collection and treating it as a snapshot. getElementsByTagName returns a live view; mutating the DOM while iterating it is a classic infinite loop.

Assuming a feature exists because you found its C++. [RuntimeEnabled] means it may not be exposed. Check the flag before concluding anything about shipping behaviour.

Reasoning about extension content scripts as "same page." Different world, different wrappers, deliberately.


Trade-offs

Generated bindings vs hand-written. Generation guarantees uniform security checks, type coercion and exception behaviour across thousands of interfaces — no one forgets a check. The cost is navigability (bi-01 Technique 4) and build time. Given the security stakes, this is one of the clearest "complexity that is obviously worth it" cases in the tree, and worth contrasting with the two-GC split (bi-04), which is much less obviously worth it.

Wrapper-per-world vs shared wrappers. Per-world isolation is a real security boundary and costs memory plus a more complex wrapper map. Shared wrappers would be simpler and would let a page tamper with extension state.

IDL expressiveness vs implementability. Every extended attribute is a feature someone needed and a permanent cost in the compiler. 69 KB of documented attributes is what that accretion looks like after two decades — a good §46 subject.


Lab

Mini-binding layer. Build a toy that demonstrates the boundary rather than reimplementing V8:

  1. A tiny IDL-like schema format: interface name, attributes with types, methods with argument types, plus a [ForcesLayout] and a [CrossOrigin] marker.
  2. A generator that emits JS accessor definitions from the schema.
  3. A "C++ side" (plain JS objects standing in) with a wrapper map enforcing identity.
  4. Type coercion per your schema, including throwing on invalid input.
  5. A [ForcesLayout] attribute whose getter runs a flush() — reuse the dirty-marking scheme from mini-browser M3.
  6. Two worlds: two wrapper maps over the same backing objects. Demonstrate that expandos do not leak between them.

Then measure: property access through your wrapper vs a plain JS property, over 1e6 iterations. Explain the gap in terms of the four costs above.


Failure Lab

  1. Break wrapper identity — create a new wrapper per access. Show two failures: expandos vanishing, and === returning false. Explain which is worse and why.
  2. Remove the world separation — show an "extension" expando becoming visible to "page" code. Write two sentences on what an attacker does with that.
  3. Layout thrash through a getter — mark an attribute [ForcesLayout], read it in a loop that also writes. Measure the complexity change.
  4. Live vs static collections — implement both; write the infinite loop; then write the version that terminates and explain the difference precisely.

Debugging Exercise

  1. In DevTools, profile a loop that reads offsetWidth each iteration. Find the layout cost. Then find it again in a Perfetto trace with the blink category, and note which view made it obvious faster.
  2. In the local checkout: pick a Web API, find its .idl, predict the generated C++ name, then confirm with git grep. Do this for one attribute and one method.
  3. Find an interface member with [Unforgeable]. Explain the attack it prevents.
  4. Find an interface member with [RuntimeEnabled] that is not on by default, and verify with --enable-blink-features on stock Chrome that the behaviour appears.

Testing & QA Considerations

  • WPT has extensive IDL-conformance tests (idlharness). Find them. What do they check that a behavioural test does not?
  • Find a test asserting cross-origin access is blocked on window. What exact error is specified?
  • If you changed an IDL file, what would need rebuilding? Answer with gn refs.

Core terminology

TermDefinition
Web IDLThe interface definition language declaring every web-exposed API
Extended attributeBracketed annotation in .idl that changes generated behaviour
Generated bindingThe C++ the IDL compiler emits; V8 calls it, and it calls Blink
WrapperThe V8 object standing for a C++ Blink object
UnwrappingRecovering the blink:: pointer from a V8 object, with a type check
IsolateA V8 instance with its own heap; workers get their own
Context / realmA global object and its intrinsics; one isolate can host many
WorldBlink's isolation unit — main world (page script) vs isolated worlds (extensions)
Unified heapV8 + Oilpan traced together via cppgc
[Unforgeable]Non-configurable property; a security primitive against shadowing
[SameObject]Identity guarantee, not just equality
[CEReactions]Custom-element reaction stack wrapping
[RuntimeEnabled]Feature-flag gating, resolved at binding-generation/runtime
Legacy* attributesThe platform labelling its own accretion — easy to find, deliberately
Interceptor vs accessorTwo ways a property access reaches native code; different cost profiles
Forced style/layout flushA getter making the pipeline current before it can answer

Mental models

A DOM property access is a cross-language call wearing a field read's syntax. That single reframe explains why DOM-heavy loops behave nothing like object-heavy loops, and why fe-03's property-shape work is irrelevant next to one getBoundingClientRect.

Some getters are commands. offsetWidth, getBoundingClientRect, scrollTop, getComputedStyle of a geometric property — these do not read a stored value; they make the pipeline current so a correct value exists. This is the mechanism behind forced synchronous layout, and knowing it turns the trigger list from something to memorise into something to derive.

The .idl file is the API's source of truth. When you need to know whether something throws, whether it is nullable, whether it returns the same object, or whether it exists behind a flag, read the IDL rather than the documentation. The extended attributes are the semantics.

Legacy* is the platform being honest about its own accretion. [LegacyLenientThis], [LegacyUnforgeable] and friends are labelled as historical by the people who maintain them — which makes them the easiest worked examples of fw-11's classification exercise.

Worlds are why extensions can coexist with page script. An isolated world gets its own wrappers for the same underlying C++ objects, so an extension's expandos are invisible to the page and vice versa. The C++ node is shared; the JS view is not.

Conversion cost is proportional to data, and strings dominate. This is the same arithmetic as fe-01's structured-clone result — crossing a boundary costs in proportion to what crosses, and designing the boundary is worth more than optimising either side of it.


Common misconceptions

  1. "element.offsetWidth is a cheap read." It is a command to make layout current. Under a write in a loop it costs a full style+layout per iteration.

  2. "The DOM is slow because C++ is slow." It is the boundary — crossing, conversion, checks — plus getters that trigger pipeline work. The C++ is fast.

  3. "IDL is documentation." It is compiled. The generated code is the behaviour.

  4. "Extended attributes are annotations." [Unforgeable] is a security control; [SameObject] is an identity guarantee; [Replaceable] changes what assignment means.

  5. "An extension and the page share JavaScript objects." They share the underlying C++ objects and get separate wrappers, in separate worlds. That separation is the security boundary.

  6. "A wrapper is created when the node is created." Wrappers are lazy — created on first access from JS — which is why wrapper cost does not show up until script touches the node.

  7. "Batching DOM reads is a micro-optimisation." It is the difference between one layout and N layouts. fe-01 measured 98× on 800 elements.


Interview talking points

  • "document.title looks like a property access and is a cross-language call with four costs: crossing, conversion, type and security checks, and side effects. The fourth is the one that produces order-of-magnitude surprises, because it's invisible in any JS-level mental model."
  • "The key reframe is that some getters are commands, not reads. offsetWidth doesn't read a stored value — it makes the pipeline current so a correct value can exist. Once you know that, the list of layout-forcing properties stops being something to memorise and becomes something to derive."
  • "I treat the .idl file as the authoritative API surface. Whether something throws, whether it's nullable, whether it returns the identical object, whether it's behind a flag — that's all declared, and the extended attributes are where the semantics hide."
  • "[Unforgeable] is a security primitive — it's why a page can't redefine window.location to fool code that reads it. Whenever I see it I read it as 'someone found a hole here.'"
  • "Web IDL's Legacy* attributes are the platform labelling its own accretion, which makes them the cleanest examples I know for teaching the difference between essential complexity and historical complexity."
  • "Boundary cost is proportional to what crosses, and strings dominate. It's the same arithmetic as structured clone at a worker boundary — designing the boundary is worth more than optimising either side of it."

Further Reading (primary sources first)

  • Web IDL specification (WHATWG) — types, extended attributes, conversion algorithms.
  • HTML Standard — WindowProxy, cross-origin property access, [Unforgeable] members.
  • third_party/blink/renderer/bindings/README.md, IDLCompiler.md, IDLExtendedAttributes.md.
  • third_party/blink/renderer/platform/bindings/ — the wrapper machinery.
  • V8 docs/blog on hidden classes, inline caches, and the interpreter/optimiser pipeline. Read only enough to reason about the boundary; V8's internals are a separate discipline.

Principal Engineer Review

  1. A colleague says "we removed jQuery so DOM access is fast now." What is right and wrong about this? Give a specific case where the change made no difference at all.

  2. Explain wrapper identity to a strong JS engineer in four sentences, including why the naive design fails.

  3. Isolated worlds give extensions a separate view of the same DOM. Enumerate what this protects and what it explicitly does not.

  4. [Unforgeable] makes a property non-configurable. Give the concrete attack it stops, and explain the cost of applying it liberally to every security-relevant property.

  5. Argue that generated bindings are essential architecture. Then identify the specific navigability cost they impose and how you would onboard an engineer despite it.

  6. offsetWidth forces layout. Design an alternative API for the same information that cannot cause forced synchronous layout. What do callers lose, and would you ship it?

  7. Given only a .idl file, what can you determine about a Web API's performance characteristics? What can you not?

  8. A team proposes exposing a new capability to the web. Walk through the layers touched, from IDL to browser process, and name the review gates it must pass.

  9. Live collections (HTMLCollection) were a design decision. Make the case they should never have existed, then the case they are the right default for the APIs that have them.

  10. You must explain to a backend engineer why "it's just a property read" took 400 ms in production. Do it in five sentences, and say what you would add to make it visible next time.


Connections to other modules

  • bi-04 — the wrapper-identity and expando requirements that make this layer necessary.
  • bi-01 — generated code is the canonical case where Code Search is not enough.
  • bi-06 — the C++ vocabulary you need to read a generated binding.
  • bi-07 / bi-08 — what a layout-forcing getter actually triggers.
  • fe-01 — forced synchronous layout measured from outside: 98× across 800 elements.
  • fe-03 — property-shape optimisation priced against one forced layout: 1,448 reads.
  • fw-11 — Legacy* attributes as the accretion classification's easiest worked example.
  • fw-12 — isTrusted and user activation are enforced at this layer.