Expert9 min read

CSS Performance and Rendering: How Browsers Paint Your Styles

9 min read
586 words
22 sections11 code blocks

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:

  1. Parse HTML into the DOM (Document Object Model)
  2. Parse CSS into the CSSOM (CSS Object Model)
  3. Combine DOM + CSSOM into the Render Tree
  4. Layout — Calculate the size and position of each element
  5. Paint — Fill in pixels (colors, borders, shadows, text)
  6. Composite — Layer painted elements together
TEXT
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
CSS
/* 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

JavaScript
// 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

CSS
/* 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:

CSS
.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
CSS
/* 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:

CSS
.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:

CSS
.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:

CSS
/* 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:

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:

CSS
/* 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.