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):

RelationshipWhy it must hold
s2.longTasks > 0 and s2.tbt > 500microtask chunking does not chunk
s2.filter ≥ 3× s0.filterchunking overhead paid, no benefit received
s3.longTasks == 0 and s4.longTasks == 0task-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 > 1000naive worker pays structured clone per keystroke
s6.boundary < 20resident worker posts only a query string
s6.inpMax is the lowest of all sevencorrect worker design wins outright
s5.inpMax > s0.inpMaxthe 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.md F1–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.md updated: any prediction you got wrong, recorded as the shape of the confusion rather than the topic