Browser Rendering Pipeline

Difficulty: Advanced

Question

Explain the browser rendering pipeline. What are reflow and repaint?

Answer

The browser rendering pipeline converts HTML/CSS/JS into pixels on screen:

1. Parse HTML -> DOM tree 2. Parse CSS -> CSSOM tree 3. Combine -> Render tree (visible elements only) 4. Layout (Reflow): calculate position and size of every element 5. Paint: fill in pixels (colors, borders, shadows, text) 6. Composite: combine layers and draw to screen

Reflow (Layout): triggered when geometry changes (width, height, position, font-size). Most expensive. Repaint: triggered when appearance changes without geometry (color, background, visibility). Composite-only: transform, opacity - cheapest, GPU-accelerated.

Performance tip: animate only transform and opacity for 60fps.

Code examples

What Triggers Reflow, Repaint, Composite

/* REFLOW (Layout) - most expensive */
/* Changes that affect geometry */
.expensive {
  width: 200px;     /* reflow */
  height: 100px;    /* reflow */
  padding: 1rem;    /* reflow */
  margin: 1rem;     /* reflow */
  font-size: 1.2rem; /* reflow */
  position: absolute; /* reflow */
  display: flex;     /* reflow */
}

/* REPAINT - moderate cost */
/* Visual changes without geometry */
.moderate {
  color: red;        /* repaint */
  background: blue;  /* repaint */
  border-color: red; /* repaint */
  box-shadow: 0 2px 4px black; /* repaint */
  visibility: hidden; /* repaint */
}

/* COMPOSITE ONLY - cheapest */
/* GPU-accelerated, no layout/paint */
.cheap {
  transform: translateX(100px); /* composite */
  opacity: 0.5;                 /* composite */
}

Every CSS property falls into one of these categories. The rendering pipeline runs in order: a reflow triggers repaint + composite. A repaint triggers composite. Composite-only skips everything.

Optimizing Rendering Performance

/* Animate with transform instead of position */
/* BAD */
.slide-bad {
  transition: left 0.3s;
}
.slide-bad.active {
  left: 100px; /* triggers reflow every frame */
}

/* GOOD */
.slide-good {
  transition: transform 0.3s;
}
.slide-good.active {
  transform: translateX(100px); /* composite only */
}

/* will-change: promote to own layer */
.animated {
  will-change: transform;
  /* Creates a new composite layer */
  /* Only use on elements that WILL animate */
}

/* contain: limit rendering scope */
.widget {
  contain: layout style paint;
  /* Browser doesn't need to recalculate
     rest of page when this changes */
}

/* content-visibility: skip offscreen rendering */
.below-fold {
  content-visibility: auto;
  contain-intrinsic-size: 500px;
  /* Browser skips rendering until visible */
}

will-change promotes an element to its own GPU layer. contain limits the scope of recalculations. content-visibility skips rendering offscreen content entirely.

Batching DOM Reads and Writes

/* JavaScript: avoid layout thrashing */

// BAD: read-write-read-write (forces multiple reflows)
const width1 = el1.offsetWidth;  // read (forces layout)
el1.style.width = '100px';       // write (invalidates)
const width2 = el2.offsetWidth;  // read (forces layout AGAIN)
el2.style.width = '200px';       // write

// GOOD: batch reads, then batch writes
const width1 = el1.offsetWidth;  // read
const width2 = el2.offsetWidth;  // read (same layout)
el1.style.width = '100px';       // write
el2.style.width = '200px';       // write (one reflow)

// BEST: use requestAnimationFrame
requestAnimationFrame(() => {
  // All DOM writes here
  // Browser batches into single reflow
  el1.style.transform = `translateX(${x}px)`;
  el2.style.opacity = progress;
});

/* CSS containment for component isolation */
.card {
  contain: layout style;
  /* Changes inside don't reflow outside */
}

Layout thrashing happens when JS interleaves DOM reads and writes. Each read forces a synchronous layout. Batch reads first, writes second.

Key points

Concepts covered

Rendering Pipeline, CSSOM, Layout, Paint, Compositing, Reflow, Repaint