The rendering pipeline
Learn how browsers turn DOM changes into pixels through style, layout, paint, compositing, and smooth transform or opacity animation.
- 01Name each rendering stageSay what style, layout, paint, and composite each do when a browser draws a page.
- 02Avoid layout work you don't needSpot a read that forces layout right away, and read all sizes before you write new styles.
- 03Choose properties that animate smoothlyUse
transformandopacityon 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:
- It works out which CSS values apply to each element.
- It measures each box and decides where it goes.
- It draws the pixels.
- It puts the drawn parts together on the screen.
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.
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, from0(invisible) to1(solid).
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.
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.
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth); // 120Line 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.
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 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.
script
box.style.width = "120px";document.body.append(box);const width = box.offsetWidth;console.log(width);Read first, then write
Layout thrashing is a pattern you can avoid. Your code does these steps again and again:
- Write a style, for example a new width.
- Read a size, for example
offsetWidth. - Write another style.
- 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.
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.
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.
Step through a small teaching model. It compares two orders: reads and writes mixed together, and all reads before all writes.
script
const second = countLayouts(["read", "read", "write", "write", "frame"]);console.log(first);console.log(second);const first = countLayouts(["write", "read", "write", "read"]);const second = countLayouts(["read", "read", "write", "write", "frame"]);console.log(first);console.log(second);writeMarks layout dirty: layout must run again.
readNeeds an up-to-date layout.
writeMarks layout dirty: layout must run again.
readNeeds an up-to-date layout.
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.
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.
.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.
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.
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.
| When you change | Work it usually needs | Why |
|---|---|---|
width, height, top, or a font | Style, then layout, then paint, then composite. | The size or position can change, so the boxes around it may move too. |
color or background-color | Style, then paint, then composite. | The boxes keep their size, but the pixels change, so paint must run again. |
transform or opacity | Style, 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: transform | No 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.
style -> layout -> paint -> composite
Use this as a first guess. Then check it with a performance trace in the browser's developer tools.
A new width usually needs these stages: style -> layout -> paint -> composite.
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.
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.
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.
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
transformto move things andopacityto fade them, when the result looks right. - Use
will-changeonly 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.
- Change
widthfrom 120px to 160px. - Change a heading's
font-size. - Change
background-color. - Change the text
color. - Animate
transform: translateX(...). - Animate
opacityfrom 1 to 0.5.
Put each card where it fits best as a first guess. Read the explanation after each choice, because real pages can add extra work.
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-changemakes 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.
| Word | What it means | Don't mix it up with |
|---|---|---|
| Layout read | Asking for a current size, such as offsetWidth. | A style write. A read can make waiting layout work run right now. |
| Paint | Drawing the pixels for text, borders, backgrounds, and images. | Composite. Composite only puts parts that are already painted together. |
| Layer | A 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 composite | A 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
Type the number this small browser example prints.
const box = document.createElement("div");
box.style.width = "75px";
document.body.append(box);
console.log(box.offsetWidth);The output is 75. The box is on the page, so the browser runs layout and can answer the read.
This is the lesson's small layout model. Predict the number it prints.
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"]));The model prints 2. Both reads come straight after a write, so both need a fresh layout.
You measure the cards in a list, then resize them. Which kind of step should you group before the style writes?
Group the layout reads first. Then group the style writes, so the browser can usually handle them in one layout at a later frame.
Which property would you choose first to slide a panel sideways, without changing its place in the page's normal layout?
Use transform for a sliding panel. It can often move a ready layer without new layout or paint.
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?
Measure first with a performance trace. It shows whether the problem is JavaScript, repeated layout, paint, composite, or something else.
Your app slides a help panel in from the right. Which change would you try first?
panel.style.transform = "translateX(0)";A transform moves the panel on the screen without changing the layout. Still check the real work in a trace, especially if the panel has complex effects.
Check your understanding
Use the stage names carefully. Keep a rule of thumb apart from a result you measured on a real page.
Question 1 of 7What is layout in the rendering pipeline?
Choose an answer to see the explanation.
Question 2 of 7What does this browser code print?
Read the code, then predictconst box = document.createElement("div"); box.style.width = "120px"; document.body.append(box); console.log(box.offsetWidth);Choose an answer to see the explanation.
Question 3 of 7Why can reading a size right after a style change be slow?
Choose an answer to see the explanation.
Question 4 of 7What does the teaching model print?
Read the code, then predictlet 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.
Question 5 of 7Which sentence about layers is true?
Choose an answer to see the explanation.
Question 6 of 7Which properties can often keep animating while the main thread is busy?
Choose an answer to see the explanation.
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,
transformandopacitycan 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.