completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

The rendering pipeline

Learn how browsers turn DOM changes into pixels through style, layout, paint, compositing, and smooth transform or opacity animation.

By the end, you can
  • 01
    Name each rendering stageSay what style, layout, paint, and composite each do when a browser draws a page.
  • 02
    Avoid layout work you don't needSpot a read that forces layout right away, and read all sizes before you write new styles.
  • 03
    Choose properties that animate smoothlyUse transform and opacity on purpose, and still check the real page, because the browser and the page can change the result.

From DOM to pixels

Your page is a tree of elements, called the DOM, plus CSS rules. The screen shows pixels: tiny colored dots. A browser can't draw an element straight from the DOM. It goes through a few steps first:

  1. It works out which CSS values apply to each element.
  2. It measures each box and decides where it goes.
  3. It draws the pixels.
  4. It puts the drawn parts together on the screen.
Definition

The rendering pipeline is the work a browser does to turn the DOM and CSS into pixels on the screen. It has four stages: style, layout, paint, and composite.

The stages always come in this order. But not every change runs every stage. A new text color doesn't move a box, so the browser doesn't need to measure again. A new width can move many boxes. So the property you change decides how much work the browser must do.

This lesson follows From HTML to running script. That lesson shows how the page and its scripts arrive. This lesson starts after that, when JavaScript or the user changes the page and the browser must draw it again.

Style, layout, paint, and composite

Style (short for style calculation) finds the final CSS value of each property for each element. It mixes together:

  • the browser's own default styles,
  • your CSS rules,
  • values an element gets from its parent (called inherited values),
  • inline styles, set right on the element.

The result is one value for each property, such as a color, a width, or a transform.

Layout measures each box and decides where it goes. It answers questions like these:

  • How wide is this card?
  • Where does its text wrap onto a new line?
  • Where does the next card start?

Many performance tools call layout reflow. Both words mean the same thing.

Set a width, then measure the boxPop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth);

Line 1 creates a new div box. Line 2 sets its width to 120px. Line 3 adds it to the page. Line 4 reads offsetWidth, the box's measured width in pixels, and prints 120. So the CSS value you wrote became a real measured size.

Paint draws the pixels for everything you can see: text, backgrounds, borders, shadows, and images.

Composite (short for compositing) takes the painted parts and puts them together on the screen, in the right order. It also cuts off parts that should be hidden. And it applies two effects you'll meet again in this lesson:

  • transform: moves, turns, or resizes an element on the screen.
  • opacity: how see-through an element is, from 0 (invisible) to 1 (solid).
Four cards in a row, joined by arrows: style finds the CSS values, layout measures the boxes, paint draws the pixels, composite puts the parts together.stylefinds theCSS valueslayoutmeasuresthe boxespaintdraws thepixelscompositeputs theparts together
The browser does the four stages in this order. Each stage uses the result of the stage before it.

These four names are a simple overview. Real browsers split the work into more steps. For example, Chrome has a step called raster, which turns paint's drawing instructions into pixels. A browser also skips the stages it doesn't need. During some animations, it skips layout and paint completely.

Real-life analogyDecorating a room

Think about decorating a room. First you choose the colors. Then you measure the room and decide where each piece of furniture goes. Then you paint the walls and the pictures. Last, you put some pictures on glass sheets. Now you can slide one sheet over another without painting anything again.

In real life: Choose the colors
In JavaScript: Style finds the CSS values
In real life: Measure the room and place the furniture
In JavaScript: Layout measures and places the boxes
In real life: Paint the walls and the pictures
In JavaScript: Paint draws the pixels
In real life: Slide glass sheets over each other
In JavaScript: Composite puts the painted parts together

Forced layout: reading right after a change

Browsers like to wait before they do rendering work. That way, they can handle many changes in one go. When you change a style, the browser only makes a note: "layout must run again". This is called marking layout dirty. Then it waits for the next frame, the next picture it draws on the screen, and measures everything once.

A forced synchronous layout happens when JavaScript changes something and then asks for a size or a position straight away. "Synchronous" means right now, before your next line of code runs. The browser can't wait for the next frame, because your code needs the answer now. These reads need an up-to-date answer:

  • offsetWidth: the element's measured width.
  • getBoundingClientRect(): the element's size and position on the screen.
  • some values from getComputedStyle(), which gives the final CSS values.
Read the width straight after setting itPop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth); // 120

Line 1 creates the box. Line 2 sets its width to 120px. Line 3 adds the box to the page. Line 4 asks for the width right now. So the browser runs layout first, and then gives back 120.

Four cards joined by arrows: write, line 2 sets the width; read, line 4 asks for the width; measure, layout runs now; 120, the printed number.writeline 2sets widthreadline 4asks widthmeasurelayoutruns now120printed
Line 4 asks for the width straight after line 2 changed it. The browser can't wait, so it measures the box right now. Then it gives back 120.

This is not always a bug. Some code really needs the current size. For example, a drag handle, a tooltip, or code that places a menu next to a button. It only becomes a problem when your code makes the browser do this work many times in one update.

Step through: set a width, then read it
Step 0 of 5Ready
Your turn: follow the blue line

Step through a small teaching model: a style change, then a read of the measured width. The model runs real lesson functions, and it only counts the layouts this lesson teaches about.

Running in
  1. script
Next: line 1
box.style.width = "120px";document.body.append(box);const width = box.offsetWidth;console.log(width);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Read first, then write

Layout thrashing is a pattern you can avoid. Your code does these steps again and again:

  1. Write a style, for example a new width.
  2. Read a size, for example offsetWidth.
  3. Write another style.
  4. Read another size.

Each read can force the browser to run layout before your code can go on. "Thrashing" means the browser keeps doing the same work over and over.

A loop that reads and writes in turnPop out in the code editor (opens in a new tab)JavaScript
const cards = [...document.querySelectorAll(".card")];for (const card of cards) {  card.style.width = `${card.offsetWidth + 10}px`;}

Line 1 collects all the cards into an array. Line 2 starts a loop over the cards. Line 3 reads the card's offsetWidth and writes a new width, 10 pixels wider, in the same line. On the next turn of the loop, the read comes straight after the last write. So the browser may have to run layout again for every card. The exact work depends on the browser, but this pattern makes extra work you don't need.

Read all the widths first, then writePop out in the code editor (opens in a new tab)JavaScript
const cards = [...document.querySelectorAll(".card")];const widths = cards.map((card) => card.offsetWidth);for (const [index, card] of cards.entries()) {  card.style.width = `${widths[index] + 10}px`;}

Line 2 reads every old width into an array called widths, before any width changes. Lines 3 and 4 then write all the new widths together. cards.entries() gives each card with its position number, index, so line 4 can find that card's old width. Now the browser can usually handle all the writes in one layout at the next frame.

The step-through below uses a small model of this. It counts how many times layout must run:

  • A write marks layout dirty.
  • A read, or the next frame, runs layout only if it is dirty. Then layout is clean again.
Two rows of cards. Mixed: write, read, write, read. Each read needs a layout, so the model counts 2 layouts. Grouped: read, read, write, write, frame. Only the frame needs a layout, so the model counts 1 layout.mixedwritereadlayoutwritereadlayout2 layoutsgroupedreadreadwritewriteframelayout1 layout
In the lesson's model, each read in the mixed order needs a fresh layout. In the grouped order, the reads come first, and one layout at the next frame handles both writes.
Step through: mixed order and grouped order
Step 0 of 6Ready
Your turn: follow the blue line

Step through a small teaching model. It compares two orders: reads and writes mixed together, and all reads before all writes.

Running in
  1. script
Next: line 1
const second = countLayouts(["read", "read", "write", "write", "frame"]);console.log(first);console.log(second);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: change the order of reads and writes
Two fixed orders to compareJavaScript
const first = countLayouts(["write", "read", "write", "read"]);const second = countLayouts(["read", "read", "write", "write", "frame"]);console.log(first);console.log(second);
Your order2 layouts
1. Write a stylewrite

Marks layout dirty: layout must run again.

2. Read a sizeread

Needs an up-to-date layout.

3. Write a stylewrite

Marks layout dirty: layout must run again.

4. Read a sizeread

Needs an up-to-date layout.

Try it yourself
Move the steps earlier or later below.

For your order on the right, the model counts 2 layouts. Put the reads before the writes to count fewer. The code on the left counts two fixed orders, so you can compare.

This small model counts a layout when a read or the next frame comes after a style write. A real browser has many more rules.

Layers and compositing

A layer is a separate painted sheet that the browser can put on the screen on its own. The browser may give an element, or part of the page, its own layer. Then the compositor, the part of the browser that does the composite stage, can move, cut, or fade that sheet. It doesn't have to run layout or paint again.

For example, the browser may put an element that moves with transform on its own layer. The CSS property will-change: transform tells the browser, "this element will probably move soon". Starting a transform animation can also make the browser prepare a layer.

Tell the browser a card will movePop out in the code editor (opens in a new tab)CSS
.card {  will-change: transform;}

Line 1 picks every element with the class card. Line 2 gives the browser a hint: the transform will probably change. Line 3 closes the rule. This hint doesn't promise a layer. It doesn't make every animation fast either. It only gives the browser information it may use.

Too many layers waste memory, because the browser must store every sheet. They can also add extra composite work. So add the hint only where you measured a real problem. Remove it when the element no longer moves.

Which changes make the browser redo layout or paint

The property you change gives a good first guess about the work. A new width, height, top, or font can change sizes and positions. So the browser usually needs all four stages: style, layout, paint, and composite.

Two more words you'll see in tools: reflow is another name for layout, and repaint means running paint again.

A small guide: which property needs which workPop out in the code editor (opens in a new tab)JavaScript
const changes = [  ["width", "layout, paint, composite"],  ["background-color", "paint, composite"],  ["transform", "often composite only"],];console.log(changes.map(([property, work]) => `${property}: ${work}`).join(" | "));

Line 1 starts an array of pairs. Each pair is a property name and the work it usually needs. Lines 2 to 4 hold the three pairs. Line 6 joins the pairs into one line, with | between them, and prints width: layout, paint, composite | background-color: paint, composite | transform: often composite only.

Three rows. width: style, layout, paint, composite. color: style, paint, composite, and layout is skipped. transform: style and composite, and layout and paint are skipped.widthstylelayoutpaintcompositecolorstyleskippedpaintcompositetransformstyleskippedskippedcomposite
A new width needs every stage. A new color skips layout. A new transform often skips layout and paint.

Each row of the table below is one kind of change. Read across to see which stages it usually needs, and why.

Which stages a property change usually needs
When you changeWork it usually needsWhy
width, height, top, or a fontStyle, then layout, then paint, then composite.The size or position can change, so the boxes around it may move too.
color or background-colorStyle, then paint, then composite.The boxes keep their size, but the pixels change, so paint must run again.
transform or opacityStyle, then often only composite.In most browsers, these can often move or fade a layer that is already painted, with no new layout or paint.
will-change: transformNo work by itself. It is only a hint.It can help the browser get a layer ready. But too many layers use a lot of memory.

A new color or background-color usually keeps every box the same size. But the pixels change, so the browser must paint again. In most browsers, transform and opacity can often skip layout and paint, and run only in the composite stage. But some things can add work back:

  • filters, such as a blur,
  • blending colors with what is behind the element,
  • parts that are cut off (clipped),
  • the elements inside it,
  • the browser's own choices.
Playground: pick one property to change
Usual stageswidth

style -> layout -> paint -> composite

Use this as a first guess. Then check it with a performance trace in the browser's developer tools.

Try it yourself
Property to change

A new width usually needs these stages: style -> layout -> paint -> composite.

This is a rule of thumb. The browser version, the element's content, and effects can add work, so measure a real page before you decide.

Animation off the main thread

The main thread is the part of the browser that does one job at a time for your page. It runs your JavaScript, your event handlers, most of style and layout, and other page work. The compositor works on its own, separately. So it can often keep a ready transform or opacity animation moving, even while the main thread is busy with JavaScript.

That is why these tools are a good fit for moving and fading things, when they animate transform or opacity:

  • CSS transitions,
  • CSS animations,
  • the Web Animations API, which lets JavaScript start an animation with element.animate().

This is a chance for smoother motion, not a promise. Keep the animated content simple. Then check a performance trace: a recording of everything the browser did, made with the browser's developer tools.

Start a transform animation from JavaScriptPop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");box.style.width = "40px";box.style.height = "40px";document.body.append(box);box.animate(  [{ transform: "translateX(0px)", opacity: 1 }, { transform: "translateX(80px)", opacity: 0.5 }],  { duration: 300, fill: "forwards" },);console.log("animation started");

Lines 1 to 4 create a 40-pixel square box and add it to the page. Lines 5 to 8 start an animation. box.animate() takes two things:

  • a list of keyframes: how the box looks at the start and at the end,
  • the timing: here, 300 milliseconds.

The box slides 80 pixels to the right and fades to half see-through. fill: "forwards" keeps the end look after the animation finishes. Line 9 prints animation started. It only shows that the animation started. It doesn't measure how smooth it was.

For moving things on everyday pages, see animation. For changing CSS from JavaScript, see styles and classes and sizes and coordinates.

Checking it in a real browser

Here is one fact you can see for yourself. When a page sets a width and then reads the size, the read gives back the new width. This doesn't show how the browser works inside. But it proves that JavaScript gets the current size when it asks.

Read the new width two waysPop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth);console.log(getComputedStyle(box).width);

Line 1 makes a box. Line 2 sets its width. Line 3 adds it to the page. Line 4 prints 120, the measured width. Line 5 prints 120px, the final CSS width from getComputedStyle(). The lesson's test runs this code in Chrome when Chrome can start, and skips this check when it can't.

Read the final CSS widthPop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(getComputedStyle(box).width);

This code is the same, except line 4 asks getComputedStyle() for the width. It prints 120px: a string that includes the unit.

Don't use this to claim how fast something is. How long rendering takes depends on the device, the page, and when the next frame comes. The only safe claim is that the read shows the new style.

Good habits for fast rendering

Start with the problem you can see. If typing, dragging, or scrolling feels jumpy, record a performance trace before you change any properties. In the trace, look for:

  • long JavaScript tasks,
  • style, layout, paint, or composite work that repeats around the moment you typed, dragged, or scrolled.

Then remove work you don't need. Keep DOM changes small. Read sizes before you write new sizes. Use transform or opacity for motion when that gives the look you want. Treat each of these as an idea to test, not a rule to follow without checking.

  • Read all the layout values first. Then write the styles that change layout.
  • Use transform to move things and opacity to fade them, when the result looks right.
  • Use will-change only for a problem you measured, and only for a short time. Don't put it everywhere.
  • Check the browser's trace. Don't guess from one property name alone.

This lesson connects to rendering performance, which uses these ideas to make everyday pages faster. It also connects to rendering and the event loop, which explains when the browser gets a chance to paint.

Sort each change by the work it usually needs
  • Change width from 120px to 160px.
  • Change a heading's font-size.
  • Change background-color.
  • Change the text color.
  • Animate transform: translateX(...).
  • Animate opacity from 1 to 0.5.
Try it yourself
0 of 6 correct

Put each card where it fits best as a first guess. Read the explanation after each choice, because real pages can add extra work.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Every DOM change lays out the whole page straight away.” Browsers often wait until a value is needed, or until the next frame.
  • “Reading a size is always bad.” Sometimes you really need the current size. The problem is a loop that reads straight after each write.
  • “will-change makes anything fast.” It is a hint that costs memory. It is not a switch that makes things fast forever.
  • “Transform is always free.” It can often skip layout and paint. But big layers and visual effects still cost work.
  • “Off the main thread means JavaScript doesn't matter.” An animation in the compositor may keep going. But long JavaScript tasks still make clicks and typing slow, and they delay other page work.

Each row of the table below is one word from this lesson. Read across to see what it means and what it is easy to mix it up with.

Rendering words that are easy to mix up
WordWhat it meansDon't mix it up with
Layout readAsking for a current size, such as offsetWidth.A style write. A read can make waiting layout work run right now.
PaintDrawing the pixels for text, borders, backgrounds, and images.Composite. Composite only puts parts that are already painted together.
LayerA painted sheet that the browser may put on the screen on its own.A promise that the element never needs paint again.
A change that can often skip to compositeA transform or opacity change that can often reuse pixels already painted.A promise of speed everywhere. The page and the browser still matter.

Practice exercises

Exercise 1 · Warm-upPredict the measured width

Type the number this small browser example prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const box = document.createElement("div");
box.style.width = "75px";
document.body.append(box);
console.log(box.offsetWidth);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upCount the layouts in mixed order

    This is the lesson's small layout model. Predict the number it prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function countLayouts(steps) {
      let dirty = false;
      let layouts = 0;
      for (const step of steps) {
        if (step === "write") dirty = true;
        if ((step === "read" || step === "frame") && dirty) {
          layouts += 1;
          dirty = false;
        }
      }
      return layouts;
    }
    console.log(countLayouts(["write", "read", "write", "read"]));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGroup your measurements

      You measure the cards in a list, then resize them. Which kind of step should you group before the style writes?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeChoose a property for motion

        Which property would you choose first to slide a panel sideways, without changing its place in the page's normal layout?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeApply the idea to a real website

          A checkout page stutters (moves in small jumps) while a coupon panel opens. What should you check before you add will-change to every card?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeChoose how to slide a panel

            Your app slides a help panel in from the right. Which change would you try first?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Use the stage names carefully. Keep a rule of thumb apart from a result you measured on a real page.

              Rendering pipeline quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What is layout in the rendering pipeline?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this browser code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const box = document.createElement("div");
                box.style.width = "120px";
                document.body.append(box);
                console.log(box.offsetWidth);

                Choose an answer to see the explanation.

              3. Question 3 of 7Why can reading a size right after a style change be slow?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the teaching model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                let dirty = false;
                let layouts = 0;
                for (const step of ["write", "read", "write", "read"]) {
                  if (step === "write") dirty = true;
                  if (step === "read" && dirty) { layouts += 1; dirty = false; }
                }
                console.log(layouts);

                Choose an answer to see the explanation.

              5. Question 5 of 7Which sentence about layers is true?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which properties can often keep animating while the main thread is busy?

                Choose an answer to see the explanation.

              7. Question 7 of 7A page moves in small jumps. What should you do first?

                Choose an answer to see the explanation.

              Key takeaways

              • Style finds the CSS values. Layout measures the boxes. Paint draws the pixels. Composite puts the painted parts together.
              • Reading a size straight after a style change can force the browser to run layout right now.
              • Read all sizes first, then write. This stops layout from running again and again in one update.
              • Layers can make composite work cheaper, but too many layers use a lot of memory.
              • In most browsers, transform and opacity can often animate smoothly in the composite stage alone.
              • Measure a real interaction before you choose a fix.

              Remember the one-liner.
              The rendering pipeline turns your page into pixels. The property you change decides how much of that work an update probably needs.

              Coming next: How input reaches your event handlers. You'll follow a click, a touch, or a key press from the moment the browser finds which element was hit to the JavaScript listener that runs.

              CompleteFrontend Clear concepts. Working examples.