CSS Performance and Rendering: How Browsers Paint Your Styles
How Browsers Render CSS
Understanding how browsers turn your CSS into pixels on the screen helps you write more performant code. The rendering process follows a specific pipeline.
The Rendering Pipeline
When a page loads, the browser processes content in these steps:
- Parse HTML into the DOM (Document Object Model)
- Parse CSS into the CSSOM (CSS Object Model)
- Combine DOM + CSSOM into the Render Tree
- Layout — Calculate the size and position of each element
- Paint — Fill in pixels (colors, borders, shadows, text)
- Composite — Layer painted elements together
HTML --> DOM --|
|--> Render Tree --> Layout --> Paint --> Composite
CSS --> CSSOM -|Layout (Reflow)
Layout calculates the geometry of every element: width, height, and position. This is the most expensive step.
Properties that trigger Layout:
- width, height, min-width, max-height
- margin, padding
- top, left, right, bottom
- display, position, float
- font-size, line-height
- border-width
- overflow
Changing any of these forces the browser to recalculate the layout of affected elements and their children.
Paint (Repaint)
Paint fills in the actual pixels. It is less expensive than layout but still costly for complex effects.
Properties that trigger Paint (but NOT Layout):
- color, background-color
- background-image
- border-color, border-style
- box-shadow, text-shadow
- outline
- visibility
Changing these repaints the affected elements but does not recalculate positions.
Composite Only
Compositing is the cheapest operation. Only two main properties trigger ONLY compositing:
- transform
- opacity
/* BEST performance: composite only */
.animated { transform: translateX(100px); }
.fade { opacity: 0.5; }
/* WORST performance: triggers layout */
.animated { left: 100px; }
.resize { width: 200px; }This is why transform and opacity are the golden properties for animation.
Avoiding Unnecessary Reflows
Batch DOM Reads and Writes
// BAD: Causes multiple reflows (read-write-read-write)
element.style.width = '100px';
const height = element.offsetHeight; // Forces reflow
element.style.padding = '10px';
const width = element.offsetWidth; // Forces reflow again
// GOOD: Batch reads, then batch writes
const height = element.offsetHeight;
const width = element.offsetWidth;
element.style.width = '100px';
element.style.padding = '10px';Use transform Instead of Position
/* BAD: Triggers layout on every frame */
@keyframes slide {
to { left: 200px; }
}
/* GOOD: Composite only, GPU-accelerated */
@keyframes slide {
to { transform: translateX(200px); }
}The will-change Property
will-change hints to the browser that an element will be animated, allowing it to optimize in advance:
.card {
will-change: transform;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
}will-change Rules
- Only apply to elements that will actually change
- Remove will-change when the animation is done (for JavaScript-triggered animations)
- Don't overuse it — each will-change element consumes GPU memory
/* BAD: Applies to everything */
* { will-change: transform; }
/* GOOD: Only on elements that animate */
.animated-card { will-change: transform, opacity; }The contain Property
contain tells the browser that an element's internal layout is independent from the rest of the page, enabling optimizations:
.card {
contain: layout style paint;
}
/* Or use the strict shorthand */
.widget {
contain: strict; /* size + layout + style + paint */
}
/* Content containment (most common) */
.component {
contain: content; /* layout + style + paint */
}contain Values
- layout: Internal layout doesn't affect external elements
- paint: Children won't be painted outside this element's bounds
- size: Element's size is not affected by children
- style: CSS counters/properties are scoped to this element
- content: Shorthand for layout + style + paint
- strict: Shorthand for size + layout + style + paint
content-visibility
content-visibility skips rendering of off-screen content entirely:
.section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}- auto: Skips rendering when off-screen, renders when approaching viewport
- contain-intrinsic-size: Provides a placeholder size so scrollbar doesn't jump
This can dramatically improve initial page load for long pages.
Selector Performance
CSS selectors are matched from right to left. The rightmost part (key selector) matters most:
/* SLOW: Browser must check every span, then walk up to find matches */
div.container ul.nav li a span { }
/* FAST: Browser directly finds .nav-text elements */
.nav-text { }Selector Performance Tips
- Keep selectors short and simple
- Avoid universal selectors in the middle of chains
- Class selectors are among the fastest
- Avoid deeply nested selectors
- IDs are fast but rarely necessary
CSS File Optimization
Critical CSS
Extract CSS needed for above-the-fold content and inline it in the HTML:
<head>
<!-- Critical CSS inlined for fast first paint -->
<style>
body { font-family: sans-serif; margin: 0; }
.header { background: #2c3e50; color: white; padding: 1rem; }
.hero { min-height: 50vh; display: flex; align-items: center; }
</style>
<!-- Full CSS loaded asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>Remove Unused CSS
Unused CSS adds to file size and parsing time. Tools like PurgeCSS can automatically remove unused styles from your production builds.
Minification
Remove whitespace, comments, and shorten values:
/* Before: 847 bytes */
.card {
background-color: #ffffff;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* After minification: 134 bytes */
.card{background-color:#fff;border-radius:8px;padding:1.5rem;margin-bottom:1.5rem;box-shadow:0 1px 3px rgba(0,0,0,.1)}Summary
Browser rendering follows a pipeline: DOM + CSSOM → Render Tree → Layout → Paint → Composite. Changing layout properties (width, margin) is most expensive, paint properties (color, background) are moderate, and composite properties (transform, opacity) are cheapest. Use will-change sparingly for animated elements, contain for independent components, and content-visibility: auto for off-screen content. Keep selectors simple, inline critical CSS, and remove unused styles for optimal performance.