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
@importchains in plain CSS — each import is discovered only after the previous file downloads. Bundle instead. - Inline critical CSS for the first screen in a
styleelement 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;
}
containpromises the browser that an element's inside doesn't affect the outside, so changes inside can be handled locally.content-visibility: autoskips rendering off-screen sections entirely until they're near the viewport — a big win on long pages.contain-intrinsic-sizegives them a placeholder height (autoremembers 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,widthandheightattributes). - Don't insert content above what the user is reading.
- Use fallback font metrics that match the web font.
- Animate with
transforminstead of changingtoporheight.
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".