Verification Checkpoints
A checkpoint passes on observed output, not on having read the explanation. Where a range is given, the range is the pass condition — exact milliseconds are machine-specific.
Reference environment: Chrome for Testing 149.0.7827.55, arm64 macOS, 6× CPU throttle.
Checkpoint 1 — Event ordering is stable and differs by dispatch path
npm run ordering
=== A. EVENT ORDERING (40 trials each) ===
real click 40/40 A F B C G H D E
.click() 40/40 A F G B C H E D
Pass: both rows are 40/40 (or ≥ 38/40), and the two orders differ.
Fail — both identical: your page script is not IIFE-wrapped, or you are dispatching both
cases the same way.
Fail — any (empty) row: the harness is broken. loadTrial() should have thrown; if it did
not, window.__log is being clobbered. Do not interpret any other number until this is green.
Checkpoint 2 — The microtask checkpoint follows stack emptiness
=== B. MICROTASK CHECKPOINT vs 3 LISTENERS ON ONE ELEMENT ===
real trusted click : L1 m1 L2 m2 L3 m3
el.click() from script : L1 L2 L3 m1 m2 m3
dispatchEvent from script: L1 L2 L3 m1 m2 m3
el.click() in setTimeout : L1 L2 L3 m1 m2 m3
Pass: row 1 interleaves; rows 2–4 do not. The row that matters is the fourth. It is a genuine separate task and still does not interleave — which is what rules out "task boundary" as the explanation and leaves stack emptiness as the only one standing.
Checkpoint 3 — await costs one microtask tick
=== C. `await` TICK COST ===
sync A1 P1 A2 P2 A3 P3 P4 P5 P6
Pass: A1 appears immediately before P1, A2 before P2 — strict interleaving.
Fail — A1 appears after P3: three ticks per await. You are on a pre-2019 engine, or
reading a transpiled build where async/await was downleveled to generators. Check what your
bundler targets before concluding anything about the platform.
Checkpoint 4 — Microtask chunking renders nothing
npm run starvation
yield primitive | frames rendered | slices | wall clock
-----------------------------|-----------------|--------|-----------
await Promise.resolve() | 0 | 80 | 400ms
MessageChannel postMessage | 49 | 80 | 404ms
Pass: the microtask row is exactly 0 frames; the task row is > 20. Slice counts and wall
clock are within a few percent of each other — that equality is the point, because it proves the
two runs did the same amount of work.
Fail — microtask row > 0: your yield is not actually a microtask (an await on a real
promise that resolves from a timer is a task in disguise).
Checkpoint 5 — Forced synchronous layout is catastrophic and invisible
npm run layout
interleaved read -> write, per element | 86.2ms
batched: read all, then write all | 0.3ms
287x difference.
Pass: ratio ≥ 50×. Absolute values vary widely by machine; the ratio does not collapse.
Fail — ratio near 1: the compiler eliminated your reads, or the elements are not in the
layout tree (display:none ancestors make offsetWidth free). Confirm the elements are rendered.
Checkpoint 6 — rAF latency is frame-relative
npm run frame-timing
Pass: in the el.click() in script rows, setTimeout(0) fires at ~+0.0 ms while rAF fires
at +3 to +12 ms — a decisive, repeatable gap. In the real trusted click rows, both fire within
a fraction of a millisecond of each other and the winner column flips between runs.
The near-tie is the finding, not noise. It shows that inside a click handler the rAF callback and the frame boundary are effectively the same moment, which is why nothing is guaranteed. If you see a stable winner in the real-click rows, check your margins before claiming a rule.
Checkpoint 7 — The strategy benchmark reproduces its shape
npm run benchmark
Pass — all seven rows present, page errors: none, and these relationships hold
(absolute values will differ):
| Relationship | Why it must hold |
|---|---|
s2.longTasks > 0 and s2.tbt > 500 | microtask chunking does not chunk |
s2.filter ≥ 3× s0.filter | chunking overhead paid, no benefit received |
s3.longTasks == 0 and s4.longTasks == 0 | task-based yielding works |
s3.inpMax ≈ s4.inpMax (within ~10 ms) | both achieve responsiveness |
s4.filter > s3.filter, typically 1.5–3.5× | scheduler.yield() costs throughput |
s5.boundary > 1000 | naive worker pays structured clone per keystroke |
s6.boundary < 20 | resident worker posts only a query string |
s6.inpMax is the lowest of all seven | correct worker design wins outright |
s5.inpMax > s0.inpMax | the naive worker is worse than no worker at all |
Fail — s4 identical to s3 in every column: scheduler.yield() is unavailable and silently
fell back. The header line reports this; check it.
Fail — s5.boundary near 0: the dataset is too small for clone cost to register. Raise ROWS.
Fail — all INP values 0: no interaction was recorded. Event Timing clamps
durationThreshold to a 16 ms floor; on a fast machine with a small dataset every interaction
falls below it. Raise ROWS or THROTTLE.
Checkpoint 8 — The decomposition table shows different dominant terms
strat | input delay | processing | presentation
s0 | 1 | 27 | 55
s2 | 0 | 157 | 184
s6 | 1 | 1 | 0
Pass: the dominant term is not the same for every strategy.
This is the checkpoint with the most transfer to real work. Seven implementations of one feature produce at least three different dominant INP terms. A team looking at a single aggregate INP number cannot distinguish them and will apply one fix to three different problems.
Module completion
All eight checkpoints pass, and:
-
steps/05-failure-lab.mdF1–F3 diagnosed from evidence before reading the notes - F2 reproduced deterministically (forced response ordering), not just observed
- Three fixes implemented for F2 and ranked, with the server-side-effect case answered
-
Written answers to the Principal Review questions in
steps/06-principal-review.md -
../fe-00-roadmap/docs/learning-log.mdupdated: any prediction you got wrong, recorded as the shape of the confusion rather than the topic