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 stageExplain what style, layout, paint, and compositing contribute when a browser draws a page.
- 02Avoid needless layout workRecognize a forced layout read and group reads before writes to avoid layout thrashing.
- 03Choose animation-friendly propertiesUse transform and opacity deliberately while knowing that browser and page details still matter.
From DOM to pixels
A browser does not draw a DOM node directly. It works through a rendering pipeline: it works out which rules apply, measures boxes, draws pixels, and puts finished surfaces together on screen.
The rendering pipeline is the browser work that turns DOM and CSS into visible pixels: style, layout, paint, and composite.
These stages are connected, but they are not a promise that every change runs every stage. A text color change does not normally move a box. A width change can move many boxes. The property you change helps the browser decide what must be updated.
This follows From HTML to running script. That lesson explains how a document and its scripts arrive. This lesson begins once JavaScript or user input changes the page that the renderer will draw.
Style, layout, paint, and composite
Style calculation finds the CSS values that apply to an element. It combines browser defaults, your selectors, inherited values, and inline styles into values such as a color, display mode, width, and transform.
Layout measures boxes and places them. It answers questions such as how wide a card is, where its text wraps, and where the next card begins. Layout is also called reflow in many performance tools.
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth);Line 1 creates a box. Line 2 gives it a width. Line 3 attaches it to the document. Line 4 reads its measured width and prints 120. The read is useful because it connects a CSS value to a real layout result.
Paint draws pixels for visible parts: text, backgrounds, borders, shadows, and images. Compositing puts painted surfaces in their final order, clips them, and applies properties such as transforms and opacity.
When you decorate a room, you choose colors first, measure where furniture goes, paint what people see, then stack glass sheets with pictures so one can slide over another.
- In real life: Choose the colors
- In JavaScript: Style chooses applicable CSS values
- In real life: Measure and place furniture
- In JavaScript: Layout decides box geometry
- In real life: Paint walls and pictures
- In JavaScript: Paint draws pixels
- In real life: Slide glass sheets together
- In JavaScript: Composite arranges prepared layers
Where the analogy stops: A browser has many more rules than a room. The analogy only gives the order of the visible jobs.
Forced synchronous layout
Browsers try to delay rendering work so several changes can be handled together. A style write can mark layout dirty. That is normally helpful because the browser can wait until the next frame instead of measuring after every single change.
A forced synchronous layout happens when JavaScript asks for current geometry after making a change that may affect geometry. Values such as offsetWidth, getBoundingClientRect(), and some computed styles need an up-to-date answer.
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(box.offsetWidth); // 120Line 1 creates the element. Line 2 writes 120px. Line 3 makes the box part of the page. Line 4 asks for a number now, so the browser returns 120 after resolving the needed layout work.
This is not automatically a bug. A drag handle, a tooltip, or a positioning calculation may truly need current geometry. It becomes a problem when code makes the browser repeat that work many times in one update.
Step through a compact teaching model of a style write followed by a layout read. The model runs real lesson functions and deliberately counts only the layout decisions it teaches.
script
box.style.width = "120px";document.body.append(box);const width = box.offsetWidth;console.log(width);Read and write without thrashing
Layout thrashing is an avoidable pattern: write a style, read geometry, write another style, then read geometry again. Each read can demand that the browser catches layout up before the loop continues.
const cards = [...document.querySelectorAll(".card")];for (const card of cards) { card.style.width = `${card.offsetWidth + 10}px`;}Line 1 collects cards. Line 3 reads offsetWidth and writes a new width in the same expression. With a larger list, the next card's read can force layout after the previous card's write. The exact browser work varies, but the pattern creates unnecessary pressure.
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 the old widths before any new width is written. Lines 3 through 4 then make all writes together. The browser can usually process the grouped update at the next rendering opportunity instead of answering a fresh geometry question per card.
Replay an instrumented teaching model that compares alternating reads and writes with grouping reads before 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.
readNeeds current geometry.
writeMarks layout dirty.
readNeeds current geometry.
This teaching model counts 2 layout decisions. Put reads before writes, then use the final frame to process grouped writes once.
Layers and compositing
A layer is a separately composited surface the browser may create for an element or part of the page. A layer lets the compositor move, clip, or fade already-painted pixels without necessarily asking layout or paint to do the same work again.
For example, an element being transformed may be placed on a useful layer. will-change: transform is a hint that an element is likely to change. A transform animation can also lead the browser to prepare the right surface.
.card { will-change: transform;}Line 1 chooses the card. Line 2 gives the browser a hint about a future transform. The code does not promise a layer, and it does not make every animation fast. It only gives the browser information it may use.
Too many layers waste memory because each surface needs backing storage. They can also add compositing work. Use a hint near an interaction you measured, then remove it when it no longer describes the page.
What triggers reflow and repaint
Property names provide a useful first guess. A change to width, height, top, or a font can change geometry, so the browser generally needs style, layout, paint, and composite work. The word reflow is another name for layout.
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 creates three property examples. Lines 2 through 4 pair each one with likely work. Line 5 prints width: layout, paint, composite | background-color: paint, composite | transform: often composite only.
| Changed property | Likely work | Why |
|---|---|---|
width, height, top, font changes | Style -> layout -> paint -> composite | Size or position can change, so surrounding geometry must be considered. |
color, background-color | Style -> paint -> composite | The geometry can stay the same, but pixels need new paint. |
transform, opacity | Style -> often composite only | In most browsers these can often move or fade an existing layer without new layout or paint. |
will-change: transform | A hint, not a promise | It can help prepare a layer, but too many promoted layers use memory. |
Color and background-color normally keep the same geometry but need new pixels, so they repaint. Transform and opacity can often be composite-only in most browsers, but filters, blending, clipping, descendants, and browser choices can change the real result.
style -> layout -> paint -> composite
Use this result to form a hypothesis, then confirm it in browser DevTools.
width follows this useful rule of thumb: style -> layout -> paint -> composite.
Off-main-thread animation
The main thread runs JavaScript, event handlers, much of style and layout, and other page work. The compositor can often keep a prepared transform or opacity animation moving while a busy main thread is temporarily doing JavaScript.
This is why CSS transitions, CSS animations, and the Web Animations API are good fits for movement and fading when they animate transform or opacity. It is an opportunity, not a guarantee: keep the animated content simple and check a trace.
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 through 4 create a visible 40-pixel box. Lines 5 through 8 start an animation from one transform and opacity value to another. The last line prints animation started, proving the example started without claiming a timing result.
For an everyday guide to moving interface elements, see animation. For CSS property changes, see styles and classes and sizes and coordinates.
Browser evidence
The important observable fact is small: after a browser page writes a width and then reads geometry, the read reflects that new width. This does not reveal an engine's private implementation, but it proves that JavaScript receives current geometry 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 attaches it. Line 4 prints 120; the following computed-style version prints 120px. The lesson test runs these stable facts in Chrome when Chrome can start and skips the evidence check otherwise.
const box = document.createElement("div");box.style.width = "120px";document.body.append(box);console.log(getComputedStyle(box).width);Do not turn this fact into a clock claim. Browser timings depend on the device, document, and frame schedule. The stable lesson claim is only that the read reflects the style update.
Practical rendering habits
Start with the visible problem. If typing, dragging, or scrolling feels uneven, record a performance trace before changing properties. Look for long JavaScript tasks and repeated style, layout, paint, or composite work around the interaction.
Then reduce unnecessary work. Keep DOM changes small, read geometry before writing new geometry, and prefer transform or opacity for motion when that matches the design. These are hypotheses to test, not rules to apply blindly.
- Read layout values together before writing new layout-affecting styles.
- Use transforms for movement and opacity for fading when the visual result is correct.
- Use
will-changefor a measured, temporary reason rather than everywhere. - Inspect the browser trace; do not guess from one property name alone.
Connect this lesson with rendering performance, which applies these ideas to everyday page speed, and 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 text
color. - Animate
transform: translateX(...). - Animate
opacityfrom 1 to 0.5.
Sort the cards by the most useful 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 immediately.” Browsers often defer work until it is needed or until a frame.
- “Reading geometry is always bad.” A current measurement is sometimes necessary; repeated read-after-write loops are the concern.
- “`will-change` makes anything fast.” It is a hint with memory cost, not a permanent optimization switch.
- “Transform is always free.” It can often avoid layout and paint, but large layers and visual effects still cost work.
- “Off-main-thread means JavaScript is ignored.” A compositor animation may continue, but long JavaScript tasks still hurt input and other page work.
| Idea | What it means | Do not confuse it with |
|---|---|---|
| Layout read | Asks for current geometry such as offsetWidth. | A style write; reads can make pending layout happen now. |
| Paint | Draws pixels for text, borders, backgrounds, and images. | Compositing; composition arranges already-painted layers. |
| Layer | A separately composited surface the browser may use. | A guarantee that an element never paints again. |
| Composite-only candidate | A transform or opacity update that can often use existing pixels. | A universal performance promise; page details and browsers still matter. |
Practice exercises
Type the number printed by the small browser example.
const box = document.createElement("div");
box.style.width = "75px";
document.body.append(box);
console.log(box.offsetWidth);The output is 75. The browser resolves the attached box's layout so the geometry read can answer.
Predict the count from the compact layout model.
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 follow a write, so both demand current layout.
A list is resized after you measure its cards. Which kind of operation should you group before making style writes?
Group layout reads first. Then group style writes so a later frame can usually process them together.
Which property would you choose first to slide a panel sideways without changing its document-flow position?
Use transform for a sliding panel. It can often move a prepared layer without new layout or paint.
A checkout page stutters while a coupon panel opens. What should you inspect before adding `will-change` to every card?
Measure first with a performance trace. It can show whether the problem is JavaScript, repeated layout, paint, compositing, or something else.
Your app slides a help panel in from the right. Which update would you try first?
panel.style.transform = "translateX(0)";A transform communicates visual movement. Confirm its real work in a trace, especially when the panel contains complex effects.
Check your understanding
Use the pipeline names precisely, then separate a property rule of thumb from a measured result 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 a read after a style write be expensive?
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 statement about layers is accurate?
Choose an answer to see the explanation.
Question 6 of 7Which properties can often animate off the main thread?
Choose an answer to see the explanation.
Question 7 of 7What is the practical first response to visible jank?
Choose an answer to see the explanation.
Key takeaways
- Style finds applicable CSS values, layout decides geometry, paint draws pixels, and composite arranges prepared surfaces.
- A geometry read after a relevant style write can force the browser to resolve layout now.
- Group reads before writes to avoid repeated layout decisions in one update.
- Layers can help compositing, but too many layers consume memory.
- Transform and opacity can often animate smoothly through compositing in most browsers.
- Profile a real interaction before choosing an optimization.
Remember the one-liner.
The rendering pipeline turns page state into pixels, and the property you change decides how much work that update is likely to need.
Coming next: How input reaches your event handlers. You will follow a pointer, touch, or keyboard action from hit testing to the JavaScript listener that receives it.