# 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 |