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
| Step | Trigger examples | Cost |
|---|---|---|
| Style | Class change, resize | Recalculate computed styles |
| Layout | Geometry-affecting props | Reflow — expensive at scale |
| Paint | Color, shadows, non-composited | Fill pixels |
| Composite | transform, opacity | GPU — 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
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
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 (
scrollendevent or debounce) - Avoid heavy handlers on
scrollwithout passive listeners where applicable passive: trueon touch/wheel listeners that don’t callpreventDefault
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.