Step 2 — Does "Chunking" Actually Chunk?
Goal
Establish, by measurement, that microtask-based chunking creates zero rendering opportunities — and feel the difference in a real browser before seeing the number.
Prerequisites
- Step 1 complete
npm installinsrc/
Predict first
Two loops. Same 400 ms of work, same 5 ms slices, pausing between every slice. The only difference is how they pause:
const yieldMicro = () => Promise.resolve(); // A
const yieldTask = () => new Promise(r => { resume = r; // B
channel.port2.postMessage(0); });
Both pause constantly. Write down: how many frames does each render in 400 ms?
Run it by hand first
cd src
npm run serve # http://localhost:8080/starvation.html
Click the left button (await Promise.resolve()), watch frames drawn. Then the right button
(postMessage).
While the left run is going:
- Try selecting this text. Try scrolling.
- Watch the red square. It keeps spinning. That is not a bug — it is the most useful thing on the page.
Then measure
npm run starvation
Expected output:
=== D. MICROTASK vs TASK CHUNKING (400ms of work, 5ms slices) ===
yield primitive | frames rendered | slices | wall clock
-----------------------------|-----------------|--------|-----------
await Promise.resolve() | 0 | 80 | 400ms
MessageChannel postMessage | 49 | 80 | 404ms
What just happened
Zero frames. Not "fewer frames" — zero. The specification requires the microtask queue to be empty before the browser may proceed to the rendering steps. A loop that re-enqueues itself is never empty, so the rendering steps are never reached. You did not yield; you built a queue that refills faster than it drains.
The slice counts and wall clocks are nearly identical (80 vs 80, 400 ms vs 404 ms). That equality is what makes this a controlled experiment: both runs did the same work, in the same number of pieces, in the same time. Only the rendering differs.
The red square is the lesson most worth keeping. It animates transform, which runs on the
compositor thread, which a blocked main thread cannot stop. So during a total main-thread
freeze the page still looks alive.
Two consequences:
- A spinner is a terrible liveness indicator. "The page looks alive" and "the page is responsive" are independent claims, and most loading UI asserts the first while users care about the second.
- This is the threading rule behind "animate
transform, notleft". It is not a style preference — it decides which thread owns the animation. The same mechanism explains why one non-passivewheellistener hands your scroll performance to your worst long task: the compositor must ask the main thread whetherpreventDefault()will be called.
Scrolling may have worked during the freeze; text selection did not. Scroll is compositor; selection is main thread. You just observed the thread boundary directly.
Checkpoint
docs/verification.md Checkpoint 4.
Going deeper
WORK_MS=3000 npm run starvation # longer freeze — watch the tab become unkillable-feeling
Then ask: during the microtask run, can you open DevTools? Can you set a breakpoint? What does that tell you about where DevTools' own UI runs?