# CSS Performance & Optimization — Deep Dive
> Understanding the rendering pipeline and how to write fast CSS.
---
## 1. The CSS Rendering Pipeline
When the browser processes CSS, it goes through these stages:
```
1. Parse CSS → CSSOM (CSS Object Model)
2. Parse HTML → DOM (Document Object Model)
3. Render Tree = DOM + CSSOM (visible elements only)
4. Layout (Reflow) — Calculate positions and sizes
5. Paint — Fill in pixels (colors, images, shadows)
6. Composite — Layer pieces onto screen
```
**Goal: Push work to the compositor (stage 6) and avoid layout (stage 4).**
---
## 2. CPU vs GPU Properties
### Compositor-Only (GPU) — 60fps Safe
These properties only trigger **compositing** — the cheapest stage:
```css
.safe-animation {
transform: translateX(100px) rotate(45deg) scale(1.1);
opacity: 0.5;
filter: blur(4px) brightness(1.2); /* GPU-accelerated */
}
```
**Safe properties:** `transform`, `opacity`, `filter`, `will-change`, `background-color` (sometimes)
### Layout-Triggering (CPU) — Expensive
These force **recalculation of the entire layout tree**:
```css
.expensive {
width: 200px; /* ❌ Triggers layout */
height: 100px; /* ❌ Triggers layout */
margin: 10px; /* ❌ Triggers layout */
padding: 20px; /* ❌ Triggers layout */
border-width: 2px; /* ❌ Triggers layout */
font-size: 16px; /* ❌ Triggers layout */
top: 50px; /* ❌ Triggers layout (positioned) */
left: 50px; /* ❌ Triggers layout (positioned) */
}
```
### Paint-Only (CPU) — Moderate Cost
```css
.paint-only {
background: red; /* Triggers paint */
border-radius: 8px; /* Triggers paint */
border-color: blue; /* Triggers paint */
box-shadow: 0 2px 4px; /* Triggers paint */
outline: 2px solid; /* Triggers paint */
visibility: hidden; /* Triggers paint (still laid out) */
}
```
---
## 3. `will-change` — Pre-Hint the Browser
```css
/* ✅ Use sparingly — before animation starts */
.modal {
will-change: transform, opacity;
transition: transform 0.3s, opacity 0.3s;
}
.modal.is-open {
transform: translateY(0);
opacity: 1;
}
/* ⚠️ NEVER leave will-change permanently active */
/* It creates a new layer, consuming memory */
.element { will-change: transform; } /* ❌ Bad */
/* ✅ Remove after animation */
.element.animating { will-change: transform; }
.element.animated { will-change: auto; } /* Clean up */
```
**Rule of thumb:** Only use for elements you know will animate. Remove when done.
---
## 4. `contain` — Isolate Render Scope
Tell the browser an element is independent — skip it during layout/paint.
```css
/* Layout containment — doesn't affect siblings */
.card {
contain: layout;
}
/* Paint containment — clipped, won't draw outside */
.card { contain: paint; }
/* Style containment — descendants can't affect outside */
.card { contain: style; }
/* Content containment — no descendants affect outside */
.card { contain: content; }
/* Strict — layout + paint + style */
.card { contain: strict; }
/* Common combo for cards/lists */
.card { contain: layout style paint; }
/* For above-the-fold content that hasn't rendered yet */
.late-content {
contain: content;
contain-intrinsic-size: 0 300px; /* Reserve ~300px height */
}
```
---
## 5. CSS Specificity & Cascade
### Specificity Hierarchy
```
!important → Highest (avoid!)
Inline styles → 1,0,0,0
IDs (#header) → 1,0,0,0
Classes (.card) → 0,1,0,0
Attributes ([type]) → 0,1,0,0
Pseudo-classes (:hover) → 0,1,0,0
Elements (div) → 0,0,1,0
Pseudo-elements (::before) → 0,0,1,0
Universal (*) → 0,0,0,0
```
### `:where()` — Zero Specificity
```css
:where(h1, h2, h3) { margin: 0; } /* Specificity: 0 */
/* Can be overridden by any class, never causes specificity wars */
```
### `@layer` — Control Cascade Order
```css
@layer reset, base, components, utilities;
@layer reset {
.card { padding: 1rem; } /* Lowest priority */
}
@layer utilities {
.card { padding: 2rem; } /* Highest priority, no !important needed */
}
```
---
## 6. Minimizing Reflow/Repaint
### Batch DOM Changes
```javascript
// ❌ Bad — 3 separate reflows
element.style.width = '100px';
element.style.height = '200px';
element.style.padding = '10px';
// ✅ Good — single reflow via class toggle
element.classList.add('new-size');
// ✅ Good — CSS animation
element.classList.add('animate');
```
### Read/Write Separation
```javascript
// ❌ Bad — forces synchronous layout (read after write)
element.style.width = '100px'; // write
const h = element.offsetHeight; // read → forces layout
element.style.height = h + 'px'; // write → forces layout again
// ✅ Good — batch reads, then batch writes
const h = element.offsetHeight; // all reads first
element.style.width = '100px'; // all writes after
element.style.height = h + 'px';
```
### `requestAnimationFrame`
```javascript
// ✅ Schedule DOM updates at the right time
requestAnimationFrame(() => {
element.style.transform = `translateX(${x}px)`;
});
```
---
## 7. CSS Delivery Optimization
### Inline Critical CSS
```html
<head>
<!-- Critical above-the-fold styles inlined -->
<style>
/* Only styles needed for initial render */
.hero { /* hero styles */ }
.nav { /* nav styles */ }
</style>
<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
```
### Font Loading
```css
/* Prevent FOIT (Flash of Invisible Text) */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when ready */
}
/* Options: auto | block | swap | fallback | optional */
/* swap = best for readability, fallback = show fallback briefly, optional = rarely use custom font */
```
```html
<!-- Preload critical fonts -->
<link rel="preload" href="font.woff2" as="font"
type="font/woff2" crossorigin>
```
---
## 8. Image Performance
### Responsive Images
```html
<img src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw,
(max-width: 1024px) 50vw,
33vw"
loading="lazy"
decoding="async"
alt="Description">
```
### Modern Formats
```html
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="Description">
</picture>
```
| Format | Compression | Quality |
|--------|-------------|---------|
| AVIF | Best | Excellent |
| WebP | Very Good | Very Good |
| JPEG 2000 | Good | Good |
---
## 9. CSS Performance Anti-Patterns
### Deep Nesting
```css
/* ❌ Bad — slow selector matching */
div.container > section.main > article.post > div.content > p.text { }
/* ✅ Good — flat selectors */
.post-text { }
```
### Universal Selector
```css
/* ❌ Bad — matches EVERYTHING */
* { margin: 0; }
div * { padding: 0; }
/* ✅ Better — scoped reset */
.container > * { margin: 0; }
```
### `!important` Abuse
```css
/* ❌ Bad — breaks cascade */
.button { color: red !important; }
.button:hover { color: blue !important; }
/* ✅ Good — use @layer or higher specificity */
@layer overrides {
.button { color: red; }
}
```
### Unqualified Selectors in CSS Modules
```css
/* ❌ Slow — matches all <a> globally */
a { color: blue; }
/* ✅ Scoped */
.component a { color: blue; }
```
---
## 10. `content-visibility` — Virtual Scrolling for CSS
```css
/* Skip rendering until visible — like React.lazy for CSS */
.article-list {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Reserve ~500px per article */
}
/* For images */
.lazy-image {
content-visibility: auto;
}
```
**Effect:** Elements outside the viewport aren't rendered until scrolled into view. Massive performance boost for long pages.
---
## 11. Animation Performance Checklist
```css
/* ✅ 60fps animation — uses only transform + opacity */
.animated {
animation: slide 0.3s ease-out;
will-change: transform; /* Pre-hint */
}
@keyframes slide {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ❌ Slow animation — triggers layout */
.slow {
animation: bad-slide 0.3s ease-out;
}
@keyframes bad-slide {
from { left: -100%; } /* ❌ Layout property */
to { left: 0; }
}
```
**Golden rule:** Animate only `transform`, `opacity`, `filter`, `background-color`.
---
## 12. CSS Bundle Size Optimization
### Removing Unused CSS
- **PurgeCSS** / **PurgeCSS** — Tree-shake unused CSS based on HTML/templates
- **UnCSS** — Analyze rendered page and remove unused styles
- **CSS Modules** — Scope class names, auto-tree-shake unused
```bash
npm install -D @fullhuman/postcss-purgecss
```
```javascript
// postcss.config.js
module.exports = {
plugins: {
'@fullhuman/postcss-purgecss': {
content: ['./src/**/*.html', './src/**/*.js'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
},
},
};
```
---
## Quick Reference: Performance Tips
| Tip | Impact | Effort |
|-----|--------|--------|
| Use `content-visibility: auto` | High | Low |
| Animate `transform`/`opacity` only | High | Low |
| Lazy load images (`loading="lazy"`) | High | Low |
| Use AVIF/WebP images | High | Low |
| Inline critical CSS | Medium | Medium |
| `font-display: swap` | Medium | Low |
| `contain: layout` on cards | Medium | Low |
| Remove unused CSS (PurgeCSS) | Medium | Medium |
| Use `@layer` instead of `!important` | Low | Low |
| Avoid deep selectors | Low | Low |
| `will-change` before animation | Low | Low |
| Batch DOM reads/writes | High (JS) | Medium |