Home / Articles

Reflow, Repaint, Composite: What Each CSS Change Costs the Browser

Follow HTML and CSS through DOM, CSSOM, layout, paint and compositing, and learn why width changes cost more than color changes and how to avoid layout thrashing.

Publisher: REACTAPP.TOP16 min read3720 words

This article is published in English.

Sources: Browser Rendering Pipeline Explained: From HTML to Pixels + Reflow vs Repaint

A single line of markup such as a heading looks trivial, yet the browser performs a long chain of work before it becomes pixels on a screen. Knowing that chain is what lets you answer practical questions: why resizing an element can slow a page down, why animations are usually written with transform, why one change can ripple through dozens of other elements, and why heavy DOM manipulation produces a stuttering interface. This guide walks through each stage of the rendering pipeline, then uses that model to explain the difference between reflow and repaint and the habits that keep frames fast.

<h1>Hello World</h1>

The pipeline at a glance

At its core, the rendering pipeline is the process that turns three kinds of input, markup, styles and scripts, into an image the display can show.

HTML + CSS + JavaScript
          ↓
       Pixels
          ↓
       Screen

Broken into its individual stages, the sequence looks roughly like this:

Parse HTML
   ↓
Build DOM
   ↓
Parse CSS
   ↓
Build CSSOM
   ↓
Create Render Tree
   ↓
Calculate Styles
   ↓
Layout / Reflow
   ↓
Paint / Repaint
   ↓
Composite
   ↓
Pixels on Screen

When people talk about how quickly a page reaches its first visible content, they usually call this sequence the Critical Rendering Path. Keep in mind that it is a model, not a specification of any one engine. Chrome, Firefox and Safari implement the stages differently, and on later updates they skip stages whenever they can prove the work is unnecessary. The model is still accurate enough to reason about performance, which is what matters for day-to-day frontend work.

Stage 1: parsing HTML into the DOM

Everything starts with the HTML document the server returns, for example a small page with one heading and one paragraph:

<!DOCTYPE html>
<html>
  <body>
    <h1>Hello World</h1>
    <p>Welcome to my website.</p>
  </body>
</html>

The parser reads this markup and produces the Document Object Model, a tree of nodes in which every element and every piece of text has a place. Drawn out, the tree for the document above looks like this:

HTML

<html>
  │
  └── body
       │
       ├── h1
       │    └── "Hello World"
       │
       └── p
            └── "Welcome..."

The DOM captures what the document contains and how it is nested. It is also the interface your scripts work against, so a call like the one below walks this tree to find a node:

document.querySelector("h1");

Parsing is incremental. The browser does not wait for the entire file before it starts building nodes, which is why a long page can begin rendering while the rest of it is still arriving.

Stage 2: parsing CSS into the CSSOM

Structure alone does not say anything about appearance. For that the browser needs the stylesheets, such as these two simple rules:

h1 {
  color: blue;
  font-size: 32px;
}

p {
  color: gray;
}

The CSS is parsed into a second structure, the CSS Object Model. A useful way to separate the two trees is by the question each one answers:

DOM
 ↓
"What exists on the page?"

CSSOM
 ↓
"How should those things look?"

Rendering needs both answers. This is also why stylesheets are render-blocking.

Stage 3: combining both trees into the render tree

With the DOM and the CSSOM available, the browser merges them into a render tree.

        DOM
         +
       CSSOM
         ↓
    Render Tree

The render tree only holds what will actually be drawn, together with the styles that apply to it. Take this fragment:

<div>
  <h1>Hello</h1>
  <p>World</p>
</div>

After merging, it can be pictured as a tree of visible boxes carrying their style information:

Render Tree

div
│
├── h1
│   ├── font-size: 32px
│   └── color: blue
│
└── p
    └── color: gray

Anything that takes no part in rendering is left out. An element with display: none has no entry at all, which is different from visibility: hidden: a hidden element still occupies space, so it stays in the tree and still participates in layout.

Stage 4: calculating computed styles

Next the browser works out the final, computed style for every element that will be rendered. Real stylesheets contain overlapping rules like these:

.card {
  width: 300px;
}

.card p {
  font-size: 16px;
}

For each element the engine has to settle three questions in order:

Which rules apply?
        ↓
Which rule wins?
        ↓
What is the final computed style?

Answering them means applying all of CSS's resolution rules:

  • the cascade
  • inheritance from parent elements
  • selector specificity
  • conversion to computed values
  • styles that depend on media queries

The result is one resolved set of properties per element:

DOM + CSSOM
     ↓
Style Calculation
     ↓
Computed Styles

Stage 5: layout, where geometry is decided

At this point the browser knows what to show and what it should look like. What it does not yet know is where each piece goes and how big it is. Answering that is the job of layout, which turns a viewport into a set of positioned boxes:

Viewport
┌────────────────────────────┐
│                            │
│   Header                   │
│                            │
├──────────────┬─────────────┤
│ Sidebar      │ Content     │
│              │             │
│              │             │
└──────────────┴─────────────┘

During layout the engine computes, for every box in the render tree:

  • width and height
  • position
  • margins and padding
  • how each box relates to its neighbours and ancestors
  • the overall geometry of the page

MDN summarises this stage as determining the size and location of each node in the render tree. Because boxes depend on each other, layout is a whole-tree problem rather than a per-element one.

Reflow: running layout again

The first geometry pass is simply called layout. Any later pass triggered by a change is usually called reflow, and some teams use the informal term relayout for the same thing:

Layout
  ↓
Initial calculation

Reflow / ReLayout
  ↓
Subsequent layout calculation

The rest of this guide uses reflow, since that is the word you will find in most performance discussions and tooling.

What sets off a reflow

Start with a box that has a fixed width:

.box {
  width: 200px;
}

Now a script changes that width at runtime:

box.style.width = "500px";

That one assignment raises a series of questions the browser has to answer before it can draw the next frame:

The box changed width.

What else is affected?
Does its parent change?
Do neighboring elements move?
Does the text wrap differently?
Does the height change?
Does the entire layout need recalculation?

Some or all of the page may therefore need a fresh layout pass, after which the downstream stages run as well:

Change width
    ↓
Style recalculation
    ↓
Layout / Reflow
    ↓
  Paint
    ↓
Composite

Properties that affect geometry

Any property that influences size or position can cause layout work. The usual suspects are:

width
height
margin
padding
top
left
right
bottom
font-size

Changing one of them can move or resize the element itself and potentially everything around it. Both of the following updates, for instance, can force the browser to recompute layout:

element.style.width = "500px";
element.style.marginLeft = "100px";

font-size belongs on the list because text size changes line height and wrapping, which changes the height of the containing block and, from there, the position of everything that follows it.

Why reflow gets expensive

Consider a typical page structure with several regions and nested content:

Page
│
├── Header
├── Navigation
├── Sidebar
├── Main Content
│    ├── Card
│    ├── Card
│    ├── Card
│    └── Table
└── Footer

If an element near the top grows taller, every card, the table and the footer below it may need new coordinates. A single geometry change can fan out across a large part of the tree:

One Geometry Change
        ↓
Layout Recalculation
        ↓
Many Elements Recalculated
        ↓
      Paint
        ↓
    Composite

That fan-out makes unnecessary layout work costly, especially when it repeats during an animation or a drag.

Stage 6: paint

Once three things are settled, the browser can start producing pixels:

What to display
       +
Where to display it
       +
How it should look

Paint turns the positioned boxes into drawing operations and, ultimately, into pixels. The painted content includes:

  • text
  • colours and backgrounds
  • borders
  • shadows
  • images
  • any other visible decoration
Layout
  ↓
Paint
  ↓
Pixels

MDN describes painting as the step that converts the boxes from layout into actual pixels on the screen.

Repaint: new pixels, same geometry

A repaint is needed when an element looks different but occupies exactly the same space. Changing the text colour is the classic case:

color: red;

A new colour does nothing to any of the following:

Width
Height
Position
Layout relationships

So the browser can skip layout entirely and run a shorter path:

Style Recalculation
       ↓
Paint / Repaint
       ↓
   Composite

Reflow versus repaint side by side

This distinction is the heart of most rendering performance advice, so it is worth stating each side precisely.

Reflow recalculates geometry

A reflow means the size or position of at least one element has to be worked out again. Setting a width is the standard example:

element.style.width = "500px";

The full chain of stages then runs:

Change
  ↓
Style
  ↓
Layout / Reflow
  ↓
Paint
  ↓
Composite

Repaint only redraws appearance

A repaint means the element looks different, but no geometry needs to be recomputed. Setting a colour is enough to trigger it:

element.style.color = "red";

Layout drops out of the chain:

Change
  ↓
Style
  ↓
Paint / Repaint
  ↓
Composite

Reduced to one line each:

RELayout / Reflow
→ Geometry changes
Repaint
→ Visual appearance changes

A button that changes width or colour

Picture a simple call-to-action button:

┌─────────────────────┐
│       Buy Now       │
└─────────────────────┘

Suppose its width is changed from the first value to the second:

width: 200px;
width: 400px;

The button now takes up twice the horizontal space, which can push siblings aside or cause the row to wrap. That is a geometry change, so the browser reflows and then repaints:

Width changes
     ↓
Geometry changes
     ↓
  Reflow
     ↓
  Repaint

Now suppose only the background colour changes instead:

background-color: blue;

The button keeps its exact size and position, and only its pixels differ:

Color changes
     ↓
Geometry unchanged
     ↓
  Repaint

The relationship is one-directional

When layout changes, boxes end up in new places, and new places need new pixels:

Layout changes
     ↓
Browser needs new pixels
     ↓
   Paint

In short, a reflow is normally followed by a repaint:

Reflow
   ↓
Usually followed by
   ↓
Repaint

The reverse does not hold. A repaint can happen on its own without any layout work:

Repaint
   ↓
Does NOT necessarily require
   ↓
Reflow

Stage 7: compositing layers into a frame

Modern engines do not always paint the whole page as a single flat image. They can split content into separate layers and assemble them in a final compositing step:

Layer 1
   +
Layer 2
   +
Layer 3
   ↓
Composite
   ↓
Final Frame

A page might, for example, have a background layer, a card on its own layer and an animated element on a third:

┌──────────────────────┐
│ Background Layer     │
│                      │
│   ┌──────────────┐   │
│   │ Card Layer   │   │
│   └──────────────┘   │
│                      │
│   ┌──────────────┐   │
│   │ Animation    │   │
│   └──────────────┘   │
└──────────────────────┘

The compositor stacks these layers to produce each frame. Some elements can be promoted to their own layer, and in suitable cases browsers hand compositing to the GPU. A change that only moves or blends an existing layer can then be applied without repainting its contents.

Why transform is the default for motion

Here is a hover effect that moves a box by animating left, starting from zero:

.box {
  left: 0;
}

and ending 100 pixels to the right:

.box:hover {
  left: 100px;
}

Because left is part of the element's position in layout, every step of the transition can cost a layout pass plus a repaint. The same visual movement can be expressed with a transform instead:

.box {
  transform: translateX(0);
}

.box:hover {
  transform: translateX(100px);
}

A transform moves the already-painted element without changing its layout box, so the browser can often handle the whole animation in the compositor, with no layout or paint per frame. opacity has similar properties and is the usual choice for fades.

Treat this as a strong default rather than a law. Whether a transform really stays on the compositor depends on the element, the other styles applied to it, how layers are assigned and the specific engine.

Which stages a change actually triggers

For most style updates, the work fans out along one of two branches after style recalculation:

                    Change
                      ↓
              Recalculate Style
                      ↓
              ┌───────┴───────┐
              ↓               ↓
           Layout           No Layout
              ↓               │
            Paint             │
              ↓               ↓
              └───────┬───────┘
                      ↓
                  Composite
                     ↓
                  Screen

The fewer expensive stages a frequent update touches, the easier it is to keep rendering smooth. MDN describes the same waterfall: style recalculation, then layout, then paint, and compositing where it applies. Changes to transform or opacity on a composited layer can skip even the paint stage, which is the cheapest path of all.

The frame budget: about 16.67 ms

Most displays refresh roughly 60 times per second, which leaves a fixed slice of time for each frame:

1 second
   ÷
60 frames
   ≈
16.67ms per frame

In other words, to hold 60 FPS the browser has about this much time per frame:

16.67ms

That budget is shared by everything that has to happen before the frame is shown: your JavaScript, style calculation, layout, paint and the browser's own housekeeping. When the total runs over, the frame is dropped and the user sees a stutter:

16.67ms
   ↓
Exceeded
   ↓
Frame missed
   ↓
 Jank

The effect is most visible in interactions that produce frames continuously:

  • scrolling
  • animations
  • drag and drop
  • window or panel resizing
  • dashboards that update many values at once

MDN makes the same point: style calculation, layout and paint all have to fit inside the frame budget for animation to look smooth.

Layout thrashing: reads that force synchronous layout

A particularly common way to waste the frame budget looks harmless in code. The snippet below sets a width, reads a size, sets a height and reads again:

element.style.width = "500px";
console.log(element.offsetWidth);
element.style.height = "500px";
console.log(element.offsetHeight);

The pattern it produces is an alternation of writes and reads:

Write
 ↓
Read layout
 ↓
Write
 ↓
Read layout

Normally the browser defers layout until it is about to draw the next frame, so several style writes can be handled in one pass. Properties like offsetWidth and offsetHeight, however, must return current values. Reading one right after a write forces the engine to flush pending style and layout work synchronously, on the spot. Do that repeatedly, often inside a loop over many elements, and you get layout thrashing: many full layout passes where one would have been enough.

The fix is to group operations so that all reads happen first and all writes happen afterwards:

Bad:
Write
Read
Write
Read
Write
Read

Better:
Read
Read
Read
Write
Write
Write

How you restructure the code depends on the application, but the principle is always the same: do not make the browser recompute layout more often than it has to. The blog has a closer look at layout thrashing and live DOM collections if you want more examples of forced reflows.

Practical techniques for cheaper rendering

Animate transform and opacity

Rather than moving an element with a layout property such as this:

left: 100px;

reach for a transform where it fits the design:

transform: translateX(100px);

For fading an element out and back in, switching between these two values:

opacity: 0;
opacity: 1;

is usually a better option than animating properties that affect layout.

Keep DOM changes to a minimum

Each DOM mutation can schedule more work for the browser. Appending nodes one after another in a sequence like this:

element.appendChild(...);
element.appendChild(...);
element.appendChild(...);

is often better replaced by building the new content off-document (a DocumentFragment, for example) and inserting it once. The larger and more complex the DOM, the more each avoidable change costs.

Batch updates instead of interleaving them

Code that alternates updates with layout work ends up with a sequence like this:

Update
 ↓
Layout
 ↓
Update
 ↓
Layout
 ↓
Update
 ↓
Layout

The aim is to give the browser a group of changes it can process together:

Update
Update
Update
 ↓
Browser processes the changes

For animations and other high-frequency visual work, requestAnimationFrame() schedules your updates to run just before the browser renders the next frame, which keeps them aligned with the rendering cycle instead of fighting it.

Do not animate layout properties

The properties that change geometry are poor candidates for anything that runs every frame:

width
height
margin
padding
top
left
right
bottom
font-size

Each of them can trigger layout. Wherever the visual result allows it, animate these instead:

transform
opacity

Reserve space for images

An image tag without dimensions gives the browser no idea how much room to leave:

<img src="image.jpg">

When the file arrives and its real size becomes known, the surrounding content shifts and layout runs again. Declaring the intrinsic size up front avoids that:

<img
  src="image.jpg"
  width="800"
  height="600"
  alt="Example"
/>

A CSS aspect-ratio achieves the same effect for responsive images. Either way the browser can reserve the correct box before the image loads, which prevents visible layout shift.

Measure before you optimise

Not every repaint or layout pass is a problem worth solving; engines are heavily optimised and handle most of this work quickly. Base your effort on evidence with a simple loop:

Performance Problem
       ↓
    Measure
       ↓
Find Bottleneck
       ↓
    Optimize
       ↓
 Measure Again

The Performance panel in Chrome DevTools records a timeline that shows:

  • JavaScript execution
  • style recalculation
  • layout
  • paint
  • compositing work
  • long tasks
  • per-frame timing

Fix the bottleneck the profile actually reveals, not the code that merely looks expensive when you read it.

The whole pipeline in one picture

Assembled end to end, the path from source to screen looks like this:

                HTML
                  │
                  ↓
             Parse HTML
                  │
                  ↓
                 DOM
                  │
                  │
CSS ──→ Parse CSS ──> CSSOM
                  │
                  ↓
          DOM + CSSOM
                  │
                  ↓
             Render Tree
                  │
                  ↓
          Style Calculation
                  │
                  ↓
           Layout / Reflow
                  │
                  ↓
            Paint / Repaint
                  │
                  ↓
             Composite
                  │
                  ↓
           Pixels on Screen

And when something changes after the first render, the key decision point is whether geometry is affected:

CSS / DOM Change
       ↓
Style Recalculation
       ↓
Does geometry change?
       │
    ┌──┴──┐
   YES    NO
    │      │
    ↓      ↓
 Layout   Paint
    │      │
    └──┬───┘
       ↓
   Composite
       ↓
     Screen

Key takeaways

Three short summaries capture most of what matters. Reflow is the expensive branch:

RELayout / Reflow
        ↓
Changes geometry
        ↓
Width / Height / Position / Layout
        ↓
Usually leads to Paint

Repaint is the cheaper one:

     Repaint
        ↓
Changes appearance
        ↓
Color / Background / Shadow / etc.
        ↓
Doesn't necessarily require Layout

And for animation, the choice of property largely decides the cost:

Layout properties
    ↓
Potentially expensive

transform + opacity
    ↓
Often better for smooth animations

You do not need to know the internals of every engine, but you should be able to trace the general journey:

HTML
 ↓
DOM

CSS
 ↓
CSSOM

DOM + CSSOM
 ↓
Render Tree
 ↓
Style
 ↓
Layout
 ↓
Paint
 ↓
Composite
 ↓
Pixels

With that model in mind:

  • Geometry changes such as width trigger reflow and then repaint; appearance changes such as color usually need only a repaint.
  • transform and opacity can often skip layout and paint, which is why they suit animation.
  • Interleaving DOM writes with layout reads causes layout thrashing; batch reads before writes.
  • Images should declare dimensions or an aspect ratio, and every optimisation should start with a DevTools profile.

The browser will always do real work to turn code into pixels. The goal is not to eliminate that work but to stop asking for more of it than the page actually needs.