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 installinsrc/
Predict first
Seven implementations of a filter over 200,000 rows, driven by 8 real keystrokes at 6× CPU throttle:
| strategy | |
|---|---|
| s0 | fully synchronous handler |
| s1 | async handler, unchunked |
| s2 | chunked with await Promise.resolve() |
| s3 | chunked with MessageChannel, 5 ms slices |
| s4 | chunked with scheduler.yield() |
| s5 | Web Worker, dataset posted per keystroke |
| s6 | Web 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:
- 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."
- Find the dataset size at which s5 stops losing to s0. Use
ROWS=to search for it. What dominates below the crossover? - Change the s3 slice budget from 5 ms to 50 ms, then to 0.5 ms. Both are worse, for different reasons. Name both.
- 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.