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
ColumnWhat it isWhat a bad value means
longTaskstasks ≥ 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 tasksthe amount of unresponsiveness, not just its presence
jankrAF gaps > 32 msframes the user did not get. Directional only in headless
INP p75 / maxinteraction latency, per interaction> 200 ms fails the "good" threshold; > 500 ms is "poor"
dominantlargest of delay/proc/presthe diagnosis. Different values need different fixes
filterwall clock inside the filter, summedthe throughput cost of your yielding strategy
boundaryworker round-trip minus worker computestructured clone + postMessage. The worker tax
walltotal elapsed for the typing runthe 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: 0 during interaction
  • INP max under 200 ms; under 100 ms if the interaction is a keystroke
  • boundary under ~20 ms for any worker strategy
  • filter within ~2× of the unchunked baseline — you are paying a little for responsiveness

Unhealthy, and the specific diagnosis:

  • longTasks > 0 with a chunked strategy → your yield is not a task boundary (this is s2)
  • boundary in the hundreds or thousands → you are cloning your dataset per operation (this is s5)
  • filter many times the baseline with no INP improvement → pure overhead; revert
  • INP fine but jank high → the interaction is responsive but an animation is not; look at compositor-vs-main-thread properties
  • Every strategy shows the same dominant term → 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.

  1. Main thread flame chart. Find the input event 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.

  2. Interactions track. Each interaction appears as a bar with its three phases. This is the dominant column, visually. The whisker before the bar is input delay — if it is long, the cause is outside the handler.

  3. 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.

  4. User Timing track. performance.mark/measure around 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.

  5. 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 });
    

    longtask tells you that something blocked. LoAF tells you which script, plus renderStart and blockingDuration. Prefer LoAF whenever it is available.

  6. Compositor confirmation. With paint flashing / Layers on, confirm a transform animation keeps running during a long task — then change it to animate left and 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 jank column 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).