Observation Guide
How to read the evidence — in the scripts' output, and in DevTools on a real application.
Reading the benchmark 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
s2 | microtask-chunked | 8 | 836 | 8 | 184 | 184 | pres | 1192 | 0 | 3067
s6 | worker, resident | 0 | 0 | 0 | 24 | 24 | delay | 79 | 4 | 1838
| Column | What it is | What a bad value means |
|---|---|---|
longTasks | tasks ≥ 50 ms during the interaction window | > 0 means the main thread was unresponsive for ≥ 50 ms at a stretch |
TBT | Σ(duration − 50 ms) over long tasks | the amount of unresponsiveness, not just its presence |
jank | rAF gaps > 32 ms | frames the user did not get. Directional only in headless |
INP p75 / max | interaction latency, per interaction | > 200 ms fails the "good" threshold; > 500 ms is "poor" |
dominant | largest of delay/proc/pres | the diagnosis. Different values need different fixes |
filter | wall clock inside the filter, summed | the throughput cost of your yielding strategy |
boundary | worker round-trip minus worker compute | structured clone + postMessage. The worker tax |
wall | total elapsed for the typing run | the number someone will use to argue against you |
Read dominant before anything else. It routes you to a fix class:
proc→ your handler does too much. Split it: acknowledge visually now, defer the rest.pres→ DOM volume, style recalc, forced layout, or paint. Look at containment and how many elements you invalidate.delay→ something else was running when the user acted. The fix is often nowhere near the interaction you are measuring.
Healthy vs unhealthy signals
Healthy:
longTasks: 0,TBT: 0during interactionINP maxunder 200 ms; under 100 ms if the interaction is a keystrokeboundaryunder ~20 ms for any worker strategyfilterwithin ~2× of the unchunked baseline — you are paying a little for responsiveness
Unhealthy, and the specific diagnosis:
longTasks > 0with a chunked strategy → your yield is not a task boundary (this is s2)boundaryin the hundreds or thousands → you are cloning your dataset per operation (this is s5)filtermany times the baseline with no INP improvement → pure overhead; revertINPfine butjankhigh → the interaction is responsive but an animation is not; look at compositor-vs-main-thread properties- Every strategy shows the same
dominantterm → your workload is not exercising what you think
DevTools: the six things to look at
Run npm run serve, open /lab.html, DevTools → Performance, CPU: 6× slowdown, record while
typing.
-
Main thread flame chart. Find the
inputevent task. Split it into scripting vs rendering (style/layout) vs painting. Most engineers assume scripting dominates; in the 200k-row lab the render half is frequently larger. Check before optimising. -
Interactions track. Each interaction appears as a bar with its three phases. This is the
dominantcolumn, visually. The whisker before the bar is input delay — if it is long, the cause is outside the handler. -
Forced reflow warnings. DevTools flags forced synchronous layout with a red triangle and gives you the causing stack. Induce one deliberately (
run-layout.mjs's thrash path) so you know what the marker looks like before you need to find one under pressure. -
User Timing track.
performance.mark/measurearound your filter and render show up as their own lane. Instrument both separately — the point is to find out whether your intuition about the split was right. -
LoAF vs longtask. In the console:
new PerformanceObserver(l => l.getEntries().forEach(e => console.log(e.duration, e.blockingDuration, e.scripts?.map(s => [s.invoker, s.duration])) )).observe({ type: 'long-animation-frame', buffered: true });longtasktells you that something blocked. LoAF tells you which script, plusrenderStartandblockingDuration. Prefer LoAF whenever it is available. -
Compositor confirmation. With paint flashing / Layers on, confirm a
transformanimation keeps running during a long task — then change it to animateleftand watch it stop. That contrast is the threading model made visible, and it is the fastest way to explain "animate transform, not position" to a team that thinks it is a style preference.
Reading starvation.html by hand
Open /starvation.html and click each button while watching frames drawn.
- The red square keeps spinning during both runs. This is not a bug. It is a CSS animation on
transform, running on the compositor thread, which the blocked main thread cannot stop. It is the single best demonstration in the module that "the page looks alive" and "the page is responsive" are different claims — and why a spinner is a terrible liveness indicator. - Try selecting text or scrolling during the microtask run. Scrolling may still work (compositor); text selection will not (main thread).
- The frames drawn counter is rAF-driven, therefore main-thread-driven, therefore honest.
What these measurements do not tell you
State this explicitly whenever you present numbers from this module:
- One machine, one browser version. The ratios transfer; the milliseconds do not.
- Headless frame pacing is not display pacing. The
jankcolumn is directional. - Synthetic CPU throttling is not a real slow device. It scales CPU but not memory bandwidth, GPU, storage, or thermal behaviour. A real mid-tier phone will be worse in ways 6× does not model.
- A single filter workload. Selectivity, row size, and render cap all change the balance between filter and render, and therefore which term dominates.
- No network. Everything here is local compute. Real interactions usually have a request in
them, which adds a whole failure surface this module does not touch (that is
fe-17/fe-18).