Step 4 — The Strategy Benchmark

Goal

Compare seven scheduling strategies for one feature under identical conditions, and produce a written recommendation that states what the numbers do not prove.

Prerequisites

  • Steps 1–3 complete
  • npm install in src/

Predict first

Seven implementations of a filter over 200,000 rows, driven by 8 real keystrokes at 6× CPU throttle:

strategy
s0fully synchronous handler
s1async handler, unchunked
s2chunked with await Promise.resolve()
s3chunked with MessageChannel, 5 ms slices
s4chunked with scheduler.yield()
s5Web Worker, dataset posted per keystroke
s6Web Worker, dataset resident, only the query posted

Rank them for INP before running. Separately, rank them for total wall clock. If your two rankings are identical, you have probably not understood the trade-off yet.

Run

cd src
npm run benchmark          # ~90 seconds

Expected output:

 strat | strategy             | longTasks |  TBT | jank | INP p75 | INP max | dominant | filter | boundary |  wall
 s0    | sync handler         |         0 |    0 |    5 |      56 |      64 |     pres |    222 |        0 |  2098
 s1    | async, unchunked     |         0 |    0 |    4 |      56 |      64 |     pres |    221 |        0 |  2096
 s2    | microtask-chunked    |         8 |  836 |    8 |     184 |     184 |     pres |   1192 |        0 |  3067
 s3    | MessageChannel 5ms   |         0 |    0 |    0 |      32 |      32 |     pres |    957 |        0 |  1920
 s4    | scheduler.yield()    |         0 |    0 |   16 |      32 |      32 |     proc |   1358 |        0 |  1913
 s5    | worker, naive        |         8 |  913 |    8 |     200 |     200 |     pres |     34 |     1516 |  3148
 s6    | worker, resident     |         0 |    0 |    0 |      24 |      24 |    delay |     79 |        4 |  1838

Absolute numbers vary. The relationships in docs/verification.md Checkpoint 7 are the pass condition.

What just happened

s2 is worse than doing nothing. 8 long tasks, 836 ms of blocking, 3× the INP of the naive baseline, and ~5× the filter time. It paid the entire cost of chunking — clock polling, loop restructuring, a lost tight loop — and received none of the benefit, for the reason established in Step 2.

This is the most important row, because s2 passes code review. It reads as await-ing between chunks with a time budget: the careful version. A reviewer without this model approves a large regression labelled as an optimisation.

s5 vs s6: the worker is not the win — the boundary is. Same worker, same algorithm, comparable off-thread compute (34 ms vs 79 ms). s5 posts 200,000 objects per keystroke: 1516 ms of structured clone, synchronously, on the main thread — precisely what the worker was supposed to avoid. s6 keeps the data resident and posts a string: 4 ms.

A ~380× difference in boundary cost from one architectural decision. "Move it to a worker" is not an optimisation; it is a data-ownership decision. And note s5 is worse than s0 — a naive worker is worse than no worker.

s3 vs s4: identical INP, very different throughput. Both reach 32 ms INP with zero long tasks. scheduler.yield() takes 1.5–3.5× the filter wall clock, because its continuations are scheduled behind rendering by design. This is a genuine counterexample to the specification's own heuristic "prefer platform primitives" — correct default, real cost. Record it in the heuristics table in ../fe-00-roadmap/docs/learning-log.md §5.

The dominant column moves. Across seven implementations of one feature you should see at least three different dominant terms. This is the most transferable result in the module: an organisation with a single aggregate INP dashboard cannot distinguish these, and will apply one fix to three different problems.

Answer in writing

Create RESULTS.md in this module directory:

  1. Which strategy has the best INP? Which has the best total time? Are they the same? Explain the gap in terms of the event loop — not "it's faster."
  2. Find the dataset size at which s5 stops losing to s0. Use ROWS= to search for it. What dominates below the crossover?
  3. Change the s3 slice budget from 5 ms to 50 ms, then to 0.5 ms. Both are worse, for different reasons. Name both.
  4. What do your numbers not prove? Be specific. This is the section a Principal Engineer is graded on, and docs/observation.md §"What these measurements do not tell you" is the starting point, not the answer.

Optional: implement them yourself

src/web/lab.html has the same seven strategies as TODO stubs with the harness pre-wired (instrument.js). Implementing s2 yourself — after predicting its behaviour — is the single most effective exercise in this module. Serve with npm run serve.

Checkpoint

docs/verification.md Checkpoints 7 and 8.