Skip to content
AgileLogics
7 min read

CSS Performance

Prerequisites: PostCSS and build tools

CSS affects performance in two ways: loading (it blocks rendering) and rendering (style, layout, and paint work on every change).

Loading: CSS is render-blocking

The browser doesn't paint until the stylesheets in the head have loaded. So:

  • Ship less CSS. Remove unused rules (Chrome's Coverage panel shows what's unused on a page), avoid shipping a whole framework for a few components, and minify and compress.
  • Avoid @import chains in plain CSS — each import is discovered only after the previous file downloads. Bundle instead.
  • Inline critical CSS for the first screen in a style element and load the rest normally — worthwhile for landing pages, often unnecessary when the stylesheet is small and cached.
  • Split by media: <link rel="stylesheet" href="print.css" media="print"> doesn't block rendering on screen.
  • Fonts: font-display, preloading, and subsetting (see Web Fonts).

Rendering: the pipeline

Style → Layout → Paint → Composite
Changing... Triggers
width, height, margin, padding, top, font-size, display Layout, paint, composite
color, background, box-shadow, outline Paint, composite
transform, opacity, filter (usually) Composite only

Animate composite-only properties. will-change: transform hints that an element will animate, so the browser can prepare a layer — use it only on elements that actually animate, and remove it afterwards; every layer costs memory.

Layout thrashing

Reading layout (offsetWidth, getBoundingClientRect()) right after writing styles forces the browser to compute layout immediately. In a loop, that means a full layout per iteration. Batch reads, then writes — it's a JavaScript problem, but CSS is where it hurts.

Skipping work: containment and content-visibility

.widget { contain: layout paint; }

.article-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}
  • contain promises the browser that an element's inside doesn't affect the outside, so changes inside can be handled locally.
  • content-visibility: auto skips rendering off-screen sections entirely until they're near the viewport — a big win on long pages. contain-intrinsic-size gives them a placeholder height (auto remembers the real size once rendered), so the scrollbar doesn't jump. Supported in all major browsers (Chrome 85, Firefox 125, Safari 18).

Selectors are rarely the bottleneck

Browsers match selectors extremely fast. Worry about the number of elements a style change affects and expensive properties (big box-shadows and filter: blur() on large areas, animated gradients) rather than selector syntax. :has() can be costly if a broad selector like body:has(...) re-evaluates on every DOM change — measure with the Performance panel if in doubt.

Layout shift (CLS)

  • Reserve space for images, embeds, and ads (aspect-ratio, width and height attributes).
  • Don't insert content above what the user is reading.
  • Use fallback font metrics that match the web font.
  • Animate with transform instead of changing top or height.

Measure

Use Lighthouse, the Performance panel (look for long "Recalculate Style" and "Layout" entries), and the Rendering panel's "Paint flashing" and "Layout Shift Regions".