TechLead

Performance and Rendering

Write CSS that renders fast and stays smooth

CSS performance is not just about file size — it is about how the browser's rendering pipeline handles your styles at 60 fps. Understanding the render pipeline lets you write CSS that animates smoothly, avoids jank, and keeps long tasks off the main thread.

The Rendering Pipeline

Every frame the browser must potentially run through: Style → Layout → Paint → Composite. Triggering early stages is expensive; targeting the composite-only stage is cheap.

  • Layout (reflow): changing geometry — width, height, margin, padding, font-size, top, left
  • Paint: changing appearance without geometry — background-color, color, border-color, box-shadow
  • Composite only: transform, opacity — handled by the GPU, no layout or paint

GPU-Accelerated Animations

Animate transform and opacity instead of layout properties. The browser promotes elements with these animations to their own compositor layer, keeping them off the main thread.

/* SLOW — triggers layout every frame */
.bad-slide {
  position: relative;
  transition: left 0.3s;
}
.bad-slide.active { left: 100px; }

/* FAST — compositor-only, no layout */
.good-slide {
  transition: transform 0.3s ease;
}
.good-slide.active { transform: translateX(100px); }

will-change — Promote Layers Proactively

will-change tells the browser to create a compositor layer before the animation starts, eliminating the first-frame stutter. Use it sparingly — each layer consumes GPU memory.

.modal {
  will-change: transform, opacity; /* promote before animation */
}

/* Remove after animation ends to free GPU memory */
.modal.done {
  will-change: auto;
}

contain — Isolating Layout Scope

contain tells the browser that a subtree is isolated — changes inside it do not affect the outside, allowing the browser to skip layout recalculation for the rest of the page.

.widget {
  contain: layout style;  /* changes inside don't affect siblings */
}

.card-grid .card {
  contain: strict; /* layout + style + paint + size */
}

content-visibility — Skip Off-Screen Rendering

content-visibility: auto instructs the browser to skip rendering elements that are not visible in the viewport, giving dramatic load-time improvements for long pages.

.article-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 800px; /* estimated height prevents layout shift */
}

Avoiding Layout Thrashing

Reading a layout property (e.g. offsetWidth) after writing one forces a synchronous layout. Batch reads before writes.

// BAD — causes layout thrash
elements.forEach(el => {
  const width = el.offsetWidth;   // forces layout
  el.style.width = width + 10 + 'px'; // invalidates layout
});

// GOOD — read all, then write all
const widths = elements.map(el => el.offsetWidth); // one layout
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px';
});

CSS Selector Performance

Modern browsers are fast at selector matching, but a few patterns are worth avoiding on large DOMs: deeply nested selectors, universal selectors as key selectors (* + *), and overuse of :nth-child on long lists.

/* Slow key selector — browser checks every element */
.sidebar * { color: inherit; }

/* Fast — class direct lookup */
.sidebar-text { color: inherit; }

Key Metrics to Watch

  • CLS — avoid layout shifts from late-loading fonts/images (use font-display: swap, set image dimensions)
  • INP — keep main-thread tasks under 50 ms; move heavy JS off-screen
  • FCP — inline critical CSS; defer non-critical stylesheets

Continue Learning