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.
- Crossing — the call is not inlineable into V8's optimised code the way a plain JS property is.
- Conversion — every string, number, dictionary and sequence is translated between representations. Strings are the expensive one.
- Checks — Web IDL type coercion, plus security checks on cross-realm objects.
- 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 attribute | Effect |
|---|---|
[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
| Capability | Evidence you have it |
|---|---|
| Price a DOM API call | Separate crossing, conversion, checks and side effects |
Read an .idl file | Treat it as the authoritative API surface, extended attributes included |
| Explain a forced layout | Say why a getter is a command rather than a read |
| Reason about isolation | Explain 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:
- Crossing — the call itself is not inlineable into V8's optimised code the way a plain JS property is.
- Conversion — every string, number, dictionary and sequence is translated between representations. Strings are the expensive one.
- Checks — type coercion per Web IDL rules, plus security checks on cross-realm objects.
- Side effects — some getters flush style/layout.
offsetWidthis 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
.idlfile 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 attribute | Effect |
|---|---|
[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.
documentis not a JavaScript concept — it is a Blink object with a JS wrapper. Node.js has V8 and nodocument; that is the cleanest demonstration of the split.
Under the Hood
Navigation targets (derive them; do not memorise):
| Concern | Where to look |
|---|---|
| IDL definitions | .idl files throughout core/ and modules/ |
| The IDL compiler | bindings/scripts/, documented in bindings/IDLCompiler.md |
| Extended attribute semantics | bindings/IDLExtendedAttributes.md |
| Generated output listing | bindings/generated_in_core.gni, generated_in_modules.gni |
| Actual generated C++ | out/Default/gen/third_party/blink/renderer/bindings/ |
| Wrapper base class | the script-wrappable type in platform/bindings/ |
| Wrapper tracing / GC integration | platform/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.
| Tier | Directory | What it is | Trade |
|---|---|---|---|
| Ignition | src/interpreter | bytecode interpreter | starts instantly, runs slowest |
| Sparkplug | src/baseline | non-optimising baseline compiler | near-instant compile, modest speedup |
| Maglev | src/maglev | mid-tier optimising compiler | fast compile, good code |
| TurboFan | src/compiler | top-tier optimising compiler | slow 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 state | Shapes seen | Speed |
|---|---|---|
| monomorphic | 1 | fastest — direct offset |
| polymorphic | a few (~2–4) | a small check chain |
| megamorphic | many | falls 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.xis 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 tonullin 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 whybi-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
postMessageorwindow.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:
| Attribute | Effect | Why you care |
|---|---|---|
[CEReactions] | wraps the operation in the custom-element reaction stack | ordering guarantees (bi-04) |
[Unforgeable] / [LegacyUnforgeable] | non-configurable, non-shadowable | security primitive |
[CrossOrigin] | reachable across origins | a deliberate hole in the wall |
[SameObject] | must return the identical object each time | identity, not equality |
[Replaceable] | assignment shadows instead of calling a setter | legacy compatibility |
[RuntimeEnabled=X] | exists only when the flag is on | (bi-01 Technique 4) |
[SecureContext] | only in secure contexts | HTTPS-gated APIs |
[Exposed=Window,Worker] | which global(s) expose it | why an API is missing in a worker |
[PutForwards] | assignment forwards to a member of the returned object | e.g. location = url |
[LegacyLenientThis] | tolerate wrong this instead of throwing | pure 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, aDOMString, a dictionary. The surprising coercions in DOM APIs are conformance, not sloppiness. - Strings cost. Every
DOMStringcrossing the boundary is a conversion between V8's representation andWTF::String, potentially with allocation. In a hot loop readingel.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:
- A tiny IDL-like schema format: interface name, attributes with types, methods with argument
types, plus a
[ForcesLayout]and a[CrossOrigin]marker. - A generator that emits JS accessor definitions from the schema.
- A "C++ side" (plain JS objects standing in) with a wrapper map enforcing identity.
- Type coercion per your schema, including throwing on invalid input.
- A
[ForcesLayout]attribute whose getter runs aflush()— reuse the dirty-marking scheme from mini-browser M3. - 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
- Break wrapper identity — create a new wrapper per access. Show two failures: expandos
vanishing, and
===returning false. Explain which is worse and why. - Remove the world separation — show an "extension" expando becoming visible to "page" code. Write two sentences on what an attacker does with that.
- Layout thrash through a getter — mark an attribute
[ForcesLayout], read it in a loop that also writes. Measure the complexity change. - Live vs static collections — implement both; write the infinite loop; then write the version that terminates and explain the difference precisely.
Debugging Exercise
- In DevTools, profile a loop that reads
offsetWidtheach iteration. Find the layout cost. Then find it again in a Perfetto trace with theblinkcategory, and note which view made it obvious faster. - In the local checkout: pick a Web API, find its
.idl, predict the generated C++ name, then confirm withgit grep. Do this for one attribute and one method. - Find an interface member with
[Unforgeable]. Explain the attack it prevents. - Find an interface member with
[RuntimeEnabled]that is not on by default, and verify with--enable-blink-featureson 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
| Term | Definition |
|---|---|
| Web IDL | The interface definition language declaring every web-exposed API |
| Extended attribute | Bracketed annotation in .idl that changes generated behaviour |
| Generated binding | The C++ the IDL compiler emits; V8 calls it, and it calls Blink |
| Wrapper | The V8 object standing for a C++ Blink object |
| Unwrapping | Recovering the blink:: pointer from a V8 object, with a type check |
| Isolate | A V8 instance with its own heap; workers get their own |
| Context / realm | A global object and its intrinsics; one isolate can host many |
| World | Blink's isolation unit — main world (page script) vs isolated worlds (extensions) |
| Unified heap | V8 + 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* attributes | The platform labelling its own accretion — easy to find, deliberately |
| Interceptor vs accessor | Two ways a property access reaches native code; different cost profiles |
| Forced style/layout flush | A 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
-
"
element.offsetWidthis 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. -
"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.
-
"IDL is documentation." It is compiled. The generated code is the behaviour.
-
"Extended attributes are annotations."
[Unforgeable]is a security control;[SameObject]is an identity guarantee;[Replaceable]changes what assignment means. -
"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.
-
"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.
-
"Batching DOM reads is a micro-optimisation." It is the difference between one layout and N layouts.
fe-01measured 98× on 800 elements.
Interview talking points
- "
document.titlelooks 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.
offsetWidthdoesn'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
.idlfile 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 redefinewindow.locationto 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
-
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.
-
Explain wrapper identity to a strong JS engineer in four sentences, including why the naive design fails.
-
Isolated worlds give extensions a separate view of the same DOM. Enumerate what this protects and what it explicitly does not.
-
[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. -
Argue that generated bindings are essential architecture. Then identify the specific navigability cost they impose and how you would onboard an engineer despite it.
-
offsetWidthforces layout. Design an alternative API for the same information that cannot cause forced synchronous layout. What do callers lose, and would you ship it? -
Given only a
.idlfile, what can you determine about a Web API's performance characteristics? What can you not? -
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.
-
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. -
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—isTrustedand user activation are enforced at this layer.