bi-10 — Analysis

Required invariants

  1. The active tree can produce frames without the main thread. This is the property that makes scroll and compositor animations survive jank.
  2. Half-rastered content is never shown. The pending tree activates only when its tiles are ready.
  3. A CompositorFrame is instructions, not pixels — render passes of draw quads.
  4. The compositor must be able to decide input handling alone, using hit-test data produced at paint time. A non-passive listener removes that ability.
  5. Damage is tracked, so the GPU redraws only what changed.

Where a frame is lost

Stage overrunsSymptomUsual cause
main-thread framejank, input delaylong tasks, forced sync layout, heavy rAF
commitperiodic hitcheshuge layer trees, many property changes
rastercheckerboardingexpensive paint, large layers
activationstale contentraster outran
draw / aggregatedropped framesGPU contention, too many surfaces

The budget, stated honestly

60 Hz → 16.7 ms      120 Hz → 8.3 ms      144 Hz → 6.9 ms

Within that: input, rAF, observers, style, layout, pre-paint, paint, commit, raster, activate, draw, aggregate, present. Your JavaScript is one term. The common failure is optimising script from 8 ms to 5 ms while style+layout costs 9 ms and reporting no improvement.

Two properties that are easy to state and hard to internalise:

  • High refresh halves the budget, not the costs. Raster and GPU work do not get cheaper at 120 Hz. Content comfortable at 60 Hz can be visibly janky at 120.
  • Frame budgets are cliffs, not slopes. Overrun by 1 ms and you lose a whole frame. This is why p95 matters far more than mean, and why an average-based dashboard hides the problem.

Checkerboarding is correct behaviour

Blank regions during fast scroll mean the compositor drew a consistent frame using what was ready rather than blocking. The alternative — stall until raster completes — is worse. This is a deliberate choice of stale but consistent over correct but late, and it is the same trade the pending/active split makes one stage earlier.

The author-guarantee lever

{passive: true} is a promise not to call preventDefault(), which lets the compositor scroll without consulting the main thread. Browsers eventually made some listeners passive by default because the guarantee was almost always true and almost never declared.

The modern replacements move effects to where they can be evaluated without the main thread: IntersectionObserver, scroll-driven animations, position: sticky, CSS scroll snap. Each converts a scroll handler into something declarative.