Module 05 advanced 30 min

Rendering & Runtime Performance

Reflow, repaint, composite, layout thrashing, content-visibility, containment, and the 16ms frame budget.

After load, runtime performance determines scroll smoothness and interaction polish.

Pipeline recap

StepTrigger examplesCost
StyleClass change, resizeRecalculate computed styles
LayoutGeometry-affecting propsReflow — expensive at scale
PaintColor, shadows, non-compositedFill pixels
Compositetransform, opacityGPU — cheapest

Rule of thumb: Stay on the compositor for animations; batch layout reads/writes.

Layout thrashing (forced synchronous layout)

Reading layout (offsetWidth, getBoundingClientRect) after a write forces the browser to flush layout mid-loop.

// BAD — interleaved read/write
elements.forEach((el) => {
  el.style.width = el.offsetWidth + 10 + 'px';
});

// GOOD — batch reads, then writes
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px';
});

Layout thrashing — interleaved reads/writes vs batched

Creates 50 boxes — measures layout work duration.

content-visibility

Skip rendering work for off-screen subtrees:

.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 120px; /* placeholder height */
}

Huge wins on long feeds and dashboards — browser skips layout/paint until near viewport.

content-visibility — 2000 rows with vs without

Measures time to append 2000 list items.

CSS containment

.card {
  contain: layout style paint;
}

Isolates internal changes from propagating layout up the tree. Pair with content-visibility on lists.

will-change (use sparingly)

.flyout {
  will-change: transform;
}

Hints layer promotion — overuse wastes GPU memory. Apply before animation, remove after.

requestAnimationFrame

Sync visual updates to paint:

function animate() {
  element.style.transform = `translateX(${x}px)`;
  x += 2;
  if (x < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Don’t do layout reads inside rAF unless necessary — prefer compositor properties.

Scroll performance

  • Defer non-visual work until scroll ends (scrollend event or debounce)
  • Avoid heavy handlers on scroll without passive listeners where applicable
  • passive: true on touch/wheel listeners that don’t call preventDefault
el.addEventListener('scroll', onScroll, { passive: true });

Virtualization

For 10k+ row tables, DOM virtualization (render only visible rows) beats raw content-visibility when node count explodes memory.

Frame budget

At 60 Hz you have ~16.7 ms per frame for JS + style + layout + paint. At 120 Hz, ~8 ms. Exceeding this → dropped frames, janky scroll.

Next: Module 06 — measurement tooling and budgets.

Live on this page

TTFB
FCP
LCP
INP
CLS