completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

The critical rendering path

DOM and CSSOM to render tree, layout and paint: the minimum work before anything shows. See four real pages' critical paths, step by step, from their traces.

By the end, you can
  • 01
    Walk the critical rendering pathFollow the HTML into the DOM and the CSS into the CSSOM, then through the render tree, layout and paint.
  • 02
    Measure a page's critical pathCount its critical files, its round trips and its critical bytes, and say what each one does to the first paint.
  • 03
    Shorten the pathPick the right fix for a page: inline a little CSS, defer a script, cut unused bytes, or stop chaining files.

The minimum work before the first paint

Asha opens the Mint tea page at Leaf and Kettle again. In lesson 5.3, one slow stylesheet kept her screen blank for most of a second. This time nothing is held back on purpose. The page has one small stylesheet. But the network is slow: every request takes a 150 ms round trip, a message to the server and its reply back.

Here are three times Chrome wrote down on that visit:

Three times from one visitPop out in the code editor (opens in a new tab)JavaScript
// The Mint tea page with one small stylesheet, loaded in Chrome.// Every time is in milliseconds (ms) after the visit started.const visit = { htmlDone: 162, cssDone: 330, firstPaint: 352 }; console.log(`All the HTML had arrived at ${visit.htmlDone} ms.`);console.log(`The stylesheet arrived at ${visit.cssDone} ms.`);console.log(`The first words appeared at ${visit.firstPaint} ms.`);
  • Lines 1 and 2 are comments. Every time is counted in milliseconds (ms) from the start of the visit.
  • Line 3 holds three recorded times: when the last byte of the HTML arrived, when the stylesheet arrived, and when the first words appeared on the screen.
  • Lines 5 to 7 print them: All the HTML had arrived at 162 ms., The stylesheet arrived at 330 ms. and The first words appeared at 352 ms.

Every word of the page was on Asha's phone at 162 ms. The words reached her screen 190 ms later. What was the browser doing all that time? Was it busy building the page, or was it waiting for something?

To answer that, you need to know what the browser has to do before it can paint anything at all. It goes through five steps. To render a page is to draw it on the screen, so these steps are called the critical rendering path. They're critical because none of them can be skipped: the first paint comes only after all five.

  1. HTML to DOM. The browser reads the HTML and builds the DOM, a tree of the page's elements.
  2. CSS to CSSOM. It reads the CSS and builds the CSSOM, a tree of the style rules.
  3. The render tree. It puts the two together. The render tree holds every element that will be drawn, each with its styles.
  4. Layout. It works out where each element goes on the screen, and how big it is.
  5. Paint. It turns each element into pixels.

Here is the whole path in one picture:

The critical rendering path in five steps. The HTML becomes the DOM, and the CSS becomes the CSSOM. The two together make the render tree. Then come layout and paint. On the recorded page with a stylesheet, working out the styles took 0.2 ms, layout 4.8 ms and paint 0.1 ms.HTMLDOMCSSCSSOMrender treelayoutpaintstyle: 0.2 ms4.8 ms0.1 ms
Both arrows must arrive before the render tree can be built. The small times under the last three steps are from the Mint tea page, read from Chrome's trace.
Real-life analogyDressing the shop window

The owner of Leaf and Kettle wants to dress her shop window. First she needs two things: the stock on the shelves, and the labels that say how each item is shown. Then she picks what goes in the window, leaving the stockroom boxes behind, and measures where each item fits. Only then does she put it all on show.

In real life: The stock on the shelves
In JavaScript: The DOM: every element of the page
In real life: The labels that say how each item is shown
In JavaScript: The CSSOM: every style rule
In real life: Picking what goes in the window
In JavaScript: The render tree: the elements that will be drawn, with their styles
In real life: Measuring where each item fits
In JavaScript: Layout: each element's size and position
In real life: Putting it all on show
In JavaScript: Paint: the pixels on the screen
The main idea

The first paint needs the DOM and the CSSOM. So it waits for every file those two are built from: the HTML, and the CSS. Those files are the critical path. Fewer of them, fewer trips to fetch them, and fewer bytes in them all mean an earlier first paint.

The rest of the lesson answers the question about Asha's 190 ms, one part at a time:

  • how the five steps work, and how long each one took on her page;
  • how to measure the path, by its files, its round trips and its bytes;
  • when exactly the first paint happens;
  • how to make the path shorter.

For the same story in brief, see the system design lesson The critical rendering path. For how the browser reads the HTML as it arrives, see HTML parsing and the preload scanner.

DOM, CSSOM, and the render tree

HTML becomes the DOM

The browser starts with the HTML. Here is the body of the Mint tea page, the part a visitor sees:

The body of the Mint tea pageHTML
<body>  <header>Leaf and Kettle</header>  <main>    <h1>Mint tea</h1>    <p>Fresh mint leaves, picked this week, dried in the shade and packed by hand. Steep a spoonful for four minutes.</p>    <p class="note">Shown only to staff.</p>  </main></body>
  • Line 2 is the header, with the shop's name.
  • Lines 3 to 7 are the main part: a heading on line 4, and two paragraphs on lines 5 and 6.
  • Line 6 has the class note. It's a message for staff only. The CSS will hide it.

The browser reads these tags and turns each one into an object called an element. Then it links the elements into a tree. The body holds the header and the main part, and the main part holds the heading and the two paragraphs. That tree is the DOM, short for Document Object Model.

Ilya Grigorik's web.dev article sums up how the DOM is built in one line: "Bytes → characters → tokens → nodes → object model." In plain words:

  • the bytes from the network become characters;
  • the characters become tags, which the article calls tokens;
  • each tag becomes an object, a node;
  • the nodes are linked into a tree, the DOM.

The browser doesn't wait for the last byte to start. It builds the DOM bit by bit as the HTML arrives, as lesson 5.1 showed.

CSS becomes the CSSOM

Next comes the CSS. Here is the page's whole stylesheet, styles.css:

The Mint tea page's stylesheetCSS
body { margin: 0; font-family: system-ui, sans-serif; color: #2b2a27; background: #fbf8f1; }header { padding: 16px; font-weight: 700; background: #3f6f31; color: #fff; }main { padding: 16px; max-width: 36em; }.note { display: none; }
  • Line 1 sets the font and the colors for the body.
  • Line 2 makes the header green, with white text.
  • Line 3 adds some space around the main part.
  • Line 4 hides every element with the class note.

The browser turns the CSS into a tree too. It's called the CSSOM, short for CSS Object Model. Why a tree? Because styles flow downwards. The font on line 1 is set on the body, so everything inside the body gets it too, unless a more specific rule says otherwise. web.dev says the rules "cascade down".

There's one big difference from the DOM. The browser can't use half a stylesheet. A rule near the end can change a rule near the top. So the CSSOM can only be used once the whole stylesheet has arrived and been read. That's why a stylesheet holds back the first paint, as lesson 5.3 showed.

The render tree holds only what will be drawn

Now the browser has both trees. It combines them into the render tree, in three steps:

  1. It walks the DOM from the top, one element at a time.
  2. For each element, it finds the CSS rules that match, and works out its final styles.
  3. It keeps the elements that will be drawn, and leaves out the rest.

Here is that last step for the Mint tea page's body:

Which elements are drawnPop out in the code editor (opens in a new tab)JavaScript
// The elements in the Mint tea page's body.const elements = ["body", "header", "main", "h1", "p", "p.note"];// The stylesheet's rule .note { display: none; } hides one of them.const hidden = ["p.note"]; const renderTree = elements.filter((element) => !hidden.includes(element));console.log(`The body has ${elements.length} elements in the DOM.`);console.log(`Only ${renderTree.length} go into the render tree: ${renderTree.join(", ")}.`);
  • Line 2 lists the body's elements.
  • Lines 3 and 4 say which one is hidden. Line 4 of the stylesheet gives p.note display: none.
  • Line 6 keeps every element that isn't hidden. Those make the render tree.
  • Line 7 prints The body has 6 elements in the DOM.
  • Line 8 prints Only 5 go into the render tree: body, header, main, h1, p.

The staff note is still in the DOM, so JavaScript can find it. But it won't be drawn, so it isn't in the render tree.

And the head? Every browser starts with a few default styles, which the HTML standard lists. One of them gives the head display: none, along with tags that live in it, like title and link. So nothing in the head is drawn either. Here are both trees side by side:

Two trees side by side. On the left, the DOM of the Mint tea page: html, head, body, header, main, h1, p, p.note. The head and p.note are faded, because both have display: none. On the right, the render tree: html, body, header, main, h1, p.the DOMhtmlheaddisplay: nonebodyheadermainh1pp.notedisplay: nonethe render treehtmlbodyheadermainh1p
The faded cards have display: none: the head, with everything inside it, and the staff note. They stay in the DOM, but they don't go into the render tree.

display: none isn't the only way to hide something. visibility: hidden hides it too, but in a different way.

Real-life analogyA reserved table at a café

At a café, a table with a "Reserved" sign stays in the room. Nobody sits there, but nobody else can use its space either. A table carried out to the storeroom leaves a gap, and the other tables can move into it.

In real life: The reserved table, empty but still in the room
In JavaScript: visibility: hidden: in the render tree, as an empty box
In real life: The table carried out to the storeroom
In JavaScript: display: none: left out of the render tree
In real life: The other tables moving into the gap
In JavaScript: The elements around it moving up to fill the space

web.dev puts it this way. An element with visibility: hidden is invisible, but "still occupies space in the layout", as an empty box. display: none removes it from the render tree completely. So pick display: none when the space should close up, and visibility: hidden when it should stay.

Layout and paint

The render tree says what to draw, and how it should look. It doesn't yet say where. Layout works that out: the exact size and position of every element on this screen. A width of 50% becomes a real number of pixels. So the same page gets a different layout on a narrow phone than on a wide laptop.

Last comes paint. The browser turns each element's box into pixels. web.dev calls this step "painting" or "rasterizing".

Busy, or waiting?

Back to Asha's question. Each of the five steps runs on the browser's main thread. That's the part of the browser that runs the page's JavaScript, and does its layout and paint, one job at a time. Chrome's trace records the jobs it runs, with when each one started and how long it took. Here is the work on her page:

How long each step tookPop out in the code editor (opens in a new tab)JavaScript
// The browser's own work on the page with a stylesheet, from Chrome's trace.// Each step: its name, and how long it took in ms.const steps = [  ["Parse HTML", 0.2],  ["Parse stylesheet", 0],  ["Recalculate style", 0.2],  ["Layout", 4.8],  ["Paint", 0.1],]; const work = steps.reduce((total, step) => total + step[1], 0);console.log(`All five steps took ${work.toFixed(1)} ms.`);
  • Lines 1 and 2 are comments. They say where the numbers come from.
  • Lines 3 to 9 list the five steps, each with how long it took, in milliseconds.
  • Line 11 adds up those times.
  • Line 12 prints All five steps took 5.3 ms.

Each name is how Chrome's trace labels one step of the path:

  • Parse HTML built the DOM, and Parse stylesheet built the CSSOM. The stylesheet was so small that reading it rounds to 0 ms.
  • Recalculate style worked out every element's final styles for the render tree.
  • Layout and Paint came last. Layout was the biggest step, at 4.8 ms.

So the browser was busy for about 5 ms of Asha's 190. What happened in the rest? Look at when it started reading each file:

Waiting for the stylesheetPop out in the code editor (opens in a new tab)JavaScript
// The same page: when the browser started reading each file, in ms.const readHtml = 168.7;const readStylesheet = 331.7; console.log(`The browser waited ${(readStylesheet - readHtml).toFixed(1)} ms for the stylesheet.`);
  • Line 2 is when the browser started reading the HTML. Line 3 is when it started reading the stylesheet.
  • Line 5 subtracts one from the other. It prints The browser waited 163.0 ms for the stylesheet.

Between those two moments, the browser couldn't go on with the path. The stylesheet was still on its way over the network. That wait was more than 30 times as long as all five steps put together. In the shop, dressing the window takes a few minutes. Waiting for the labels to arrive takes all morning.

Why it matters: making the browser's work faster would save Asha almost nothing. To paint sooner, the page has to make the browser wait less. The next section shows how to measure that wait.

Critical resources, bytes, and path length

If waiting makes the first paint late, you need a way to measure the waiting. A cook planning the first plate of the day has the same problem.

Real-life analogyThe shortest way to the first plate

A new cook first has to fetch the recipe. Only once she reads it does she know what to buy. If everything is on one list, one trip to the shop gets it all, and heavy bags make that trip slower. The herbs for the garnish can come later: the first plate goes out without them.

In real life: Fetching the recipe
In JavaScript: Downloading the HTML: the first round trip
In real life: The ingredients the first plate can't go without
In JavaScript: Critical resources: the files the first paint waits for
In real life: Each trip to the shop
In JavaScript: One round trip. The number of trips is the critical path length
In real life: How heavy the shopping bags are
In JavaScript: Critical bytes: the total size of those files
In real life: The herbs for the garnish
In JavaScript: Pictures: the page paints without them, and they fill in later

Grigorik's article on analyzing the critical path measures it with three numbers:

  • Critical resources are the files that "could block initial rendering of the page". On Asha's page, that's the HTML and the stylesheet.
  • Critical path length is the "number of roundtrips" it takes to fetch them all.
  • Critical bytes are the total size of those files, as they travel over the network.

Pictures aren't on the list. In Grigorik's words, "Images don't block the initial render of the page."

To see what each number does, the course loaded the Mint tea page four ways, changing one thing at a time. The network stayed the same: a 150 ms round trip, at 5 megabits a second. A megabit is a million bits, the 0s and 1s that all data is made of. At that speed, every kilobyte takes about 1.6 ms to arrive.

A second file means a second trip

First, two versions. One has its CSS inside the HTML, in a <style> tag. The other links the CSS as its own file, like Asha's page:

CSS inside the HTML, or in its own filePop out in the code editor (opens in a new tab)JavaScript
// The same page twice: CSS inside the HTML, then CSS in its own file.const inline = { criticalFiles: 1, firstPaint: 196 };const linked = { criticalFiles: 2, firstPaint: 352 }; console.log(`CSS inside the HTML: ${inline.criticalFiles} critical file, first paint at ${inline.firstPaint} ms.`);console.log(`CSS in its own file: ${linked.criticalFiles} critical files, first paint at ${linked.firstPaint} ms.`);console.log(`The second file pushed the first paint back ${linked.firstPaint - inline.firstPaint} ms.`);
  • Lines 2 and 3 hold, for each version, how many critical files it had, and when its first paint came.
  • Lines 5 and 6 print CSS inside the HTML: 1 critical file, first paint at 196 ms. and CSS in its own file: 2 critical files, first paint at 352 ms.
  • Line 7 subtracts. It prints The second file pushed the first paint back 156 ms.

Why 156 ms? The browser can't ask for styles.css until it has read the link to it in the HTML. So the stylesheet needs a trip of its own, after the trip that brought the HTML. One round trip on this network is 150 ms. So this page's critical path length is two round trips. With the CSS inside the HTML, it's one.

Files found together share a trip

Next, the page with a stylesheet, and then the same page with a script in its head as well:

One more critical filePop out in the code editor (opens in a new tab)JavaScript
// The page with a stylesheet, then with a script in the head as well.const stylesheet = { criticalFiles: 2, firstPaint: 352 };const withScript = { criticalFiles: 3, firstPaint: 364 }; console.log(`With a stylesheet: ${stylesheet.criticalFiles} critical files, first paint at ${stylesheet.firstPaint} ms.`);console.log(`With a script too: ${withScript.criticalFiles} critical files, first paint at ${withScript.firstPaint} ms.`);console.log(`The third file pushed the first paint back ${withScript.firstPaint - stylesheet.firstPaint} ms.`);
  • Lines 2 and 3 hold each page's critical files and first paint.
  • Lines 5 and 6 print With a stylesheet: 2 critical files, first paint at 352 ms. and With a script too: 3 critical files, first paint at 364 ms.
  • Line 7 prints The third file pushed the first paint back 12 ms.

One more critical file, but only 12 ms later, not another 150. The browser found the link tag and the script tag in the same HTML, so it asked for both at once. They traveled on the same round trip, the way two things on one shopping list come home on one trip. Grigorik's article found the same: three critical resources, but still two round trips, "because we can transfer the CSS and JavaScript in parallel."

Three critical paths. CSS in the HTML: one round trip, for the page, and the first paint at 196 ms. A stylesheet: the page, then styles.css, so two round trips, and the first paint at 352 ms. A stylesheet and a script: the page, then styles.css and app.js together, still two round trips, and the first paint at 364 ms.CSS in the HTMLpagefirst paint 196 msa stylesheetpagestyles.cssfirst paint 352 msand a scriptpagestyles.cssapp.jsfirst paint 364 ms
Each column is one round trip. The stylesheet and the script were found together, so they share the second one.

Bytes take time too

Last, the stylesheet gets heavier. The course added 120 KB of rules the page never uses, like a big framework's utility classes:

A heavier stylesheetPop out in the code editor (opens in a new tab)JavaScript
// The page with a small stylesheet, then with 120 KB of unused rules added to it.const small = { criticalBytes: 1324, firstPaint: 352 };const heavy = { criticalBytes: 121214, firstPaint: 544 }; console.log(`Small stylesheet: ${small.criticalBytes} critical bytes, first paint at ${small.firstPaint} ms.`);console.log(`Heavy stylesheet: ${heavy.criticalBytes} critical bytes, first paint at ${heavy.firstPaint} ms.`);console.log(`The extra bytes pushed the first paint back ${heavy.firstPaint - small.firstPaint} ms.`);
  • Lines 2 and 3 hold each page's critical bytes, the HTML and the stylesheet together, and its first paint.
  • Lines 5 and 6 print Small stylesheet: 1324 critical bytes, first paint at 352 ms. and Heavy stylesheet: 121214 critical bytes, first paint at 544 ms.
  • Line 7 prints The extra bytes pushed the first paint back 192 ms.

The heavy page has the same two critical files and the same two round trips. Only the bytes changed. About 120,000 extra bytes, at 1.6 ms a kilobyte, take about 192 ms to arrive. And reading all those rules? It took the browser 0.8 ms. The unused CSS cost almost nothing to read, and a lot to download.

Why it matters: each of the three numbers can make the first paint late on its own. So check all three when a page paints late.

Now try all four pages yourself. Pick one, and compare how long the faint bars wait with how short the solid steps are:

Playground: four real critical paths
Files, then steps0 to 600 ms
A stylesheetrecorded
Critical files2
Critical bytes1,324 bytes
Main-thread work5.3 ms
First contentful paint352 ms
Try it yourself
The critical path

A stylesheet: 2 critical files, 1,324 bytes in all. The last one arrived at 330 ms. Then 7 main-thread steps took 5.3 ms together. The first contentful paint came at 352 ms.

Real loads in Chrome, with a 150 ms round trip added to every request at 5 Mbps. The faint bars are the critical files on their way. The solid bars are the main-thread steps from Chrome's trace, each drawn at the time it ran. The green line is the first contentful paint.

First paint and first contentful paint

So far, this lesson has called it "the first paint", as if it were one moment. The Paint Timing standard defines two:

  • First paint is the first time the browser draws anything for the page, apart from its plain default background. A colored background counts.
  • First contentful paint, or FCP, is the first time it draws text or an image from the page.

In the shop window, first paint is the lights coming on. First contentful paint is the first item you can see. When the very first frame already has text in it, both are reported for that same frame, with the same time. The browser hands both to JavaScript as paint entries, which you read with performance.getEntriesByType("paint"). Every first-paint time in this lesson's examples is the first contentful paint.

The Paint step comes before the paint

There's one more detail. The time in the paint entry isn't when the Paint step ran. After that step, the browser still has to get the finished frame onto the screen. So the standard uses the moment the frame was shown, when the browser knows it. When it doesn't, it uses the end of the rendering work.

Real-life analogyA plate on its way to the table

At a café, the cook finishes your plate and puts it on the counter. A waiter carries it to your table a moment later. You'd say your food arrived when it reached you, not when the cook finished it.

In real life: The cook finishing the plate
In JavaScript: The Paint step ending, in Chrome's trace
In real life: The walk from the kitchen to your table
In JavaScript: Getting the finished frame onto the screen
In real life: The plate reaching you
In JavaScript: The paint entry's time: the first contentful paint

Here are the two times for Asha's page:

The Paint step, and the paint entryPop out in the code editor (opens in a new tab)JavaScript
// The page with a stylesheet: the Paint step from Chrome's trace, and its paint entry.const paintStep = { start: 337.5, duration: 0.1 };const firstContentfulPaint = 352; const paintStepEnd = paintStep.start + paintStep.duration;console.log(`The Paint step ended at ${paintStepEnd} ms.`);console.log(`The first contentful paint came ${(firstContentfulPaint - paintStepEnd).toFixed(1)} ms later.`);
  • Line 2 holds the recorded Paint step: when it started, and how long it took.
  • Line 3 holds the first contentful paint, from the page's paint entry.
  • Line 5 adds the start and the length, to find when the step ended. Line 6 prints The Paint step ended at 337.6 ms.
  • Line 7 prints The first contentful paint came 14.4 ms later.

On all four recorded pages, the first contentful paint came after the Paint step. Why it matters: when you line up a trace with a paint entry, expect the paint entry to be a little later. It's the plate reaching the table.

Shortening the path

Now you can answer Asha's question. Of the 190 ms between the HTML arriving and the first words, the browser worked for about 5. Most of the rest, 163 ms, went on waiting for the stylesheet. So to paint sooner, cut the waiting. Each way to do that shrinks one of the three numbers. Read the table across: each row is one number, how to shrink it, and why that helps.

Three ways to shorten the path
What to cutHowWhy it helps
Fewer critical filesPut the small bit of CSS the first screen needs inside the HTML. Add defer to scripts. Give a stylesheet that's only for printing media="print".Every file the first paint waits for can add a round trip.
Fewer round tripsLink every stylesheet and script from the HTML, instead of chaining one file from another with @import.Files the browser finds together are fetched together.
Fewer critical bytesRemove CSS the page never uses. Then minify what's left, which removes spaces and comments, and compress it.Bytes take time on a slow network: 120 KB of unused CSS cost 192 ms.

Putting CSS inside the HTML is called inlining it. It saved the Mint tea page a whole round trip: its first paint came at 196 ms instead of 352. So why not inline every stylesheet on every page?

Real-life analogyA price list stapled to every letter

Leaf and Kettle writes to its customers every week. If it staples its price list to every letter, nobody ever has to ask for it. But every letter now carries a fresh copy, even though most customers kept last week's in a drawer.

In real life: The price list stapled to each letter
In JavaScript: CSS inlined in each page's HTML
In real life: Never having to ask for it
In JavaScript: No extra round trip before the first paint
In real life: A fresh copy in every letter
In JavaScript: The same CSS downloaded again with every page
In real life: Last week's copy, kept in a drawer
In JavaScript: A stylesheet file, kept in the browser's cache

web.dev's article on critical CSS says the same thing. Inlining "prevents the browser from caching the CSS for reuse on subsequent page loads, so it's best to use it sparingly." So inline only the small part of the CSS that the first screen needs. The rest can stay in its own file, which the browser can keep in its cache.

Try it. Choose what the page has, and watch the three numbers and the estimate change:

Playground: build a critical path
The estimate, shortenedJavaScript
function criticalPath(files, rttMs, kbps) {  const roundTrips = files.some((file) => file.critical && file.name !== "page") ? 2 : 1;  const bytes = files.filter((file) => file.critical).reduce((sum, file) => sum + file.bytes, 0);  return { roundTrips, bytes, ms: roundTrips * rttMs + (bytes * 8) / kbps };} console.log(criticalPath([{ name: "page", critical: true, bytes: 800 }], 150, 5000));
Your pageestimated
Critical files2
Round trips2
Critical bytes1,324 bytes
Network time302 ms
Try it yourself
The estimate

2 critical files, 2 round trips and 1,324 bytes. That's about 302 ms on the network before the first paint. The recorded page like this one first painted at 352 ms.

A model, like the code beside it. The HTML takes one round trip, and files linked from it take one more. On top of that, every critical byte takes its time at 5 Mbps, with a 150 ms round trip. A recorded first paint also includes the server's answer and the browser's own work.

In the panel's code, line 2 counts the round trips, line 3 adds up the critical bytes, and line 4 turns both into time.

On the path, or off it?

Here are eight things a real page might load. For each one, ask a single question: does the first paint have to wait for it? Sort them:

On the path, or off it?
  • The HTML itself
  • <link rel="stylesheet" href="main.css">
  • <script src="app.js"> in the head
  • A stylesheet the main stylesheet @imports
  • <img src="hero.jpg">
  • <script src="app.js" defer>
  • <link rel="stylesheet" href="print.css" media="print">
  • A fetch() for data, made by a deferred script
Try it yourself
0 of 8 correct

Sort each file by whether the first paint waits for it.

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

Common mix-ups

  • "Parsing and layout are the slow part." No. On the Mint tea page, all five steps took 5.3 ms. Waiting for the stylesheet took 163 ms.
  • "Every critical file adds a round trip." No. Files the browser finds together travel together. The script added 12 ms, not 150.
  • "Pictures hold back the first paint." No. They aren't critical. The page paints, and the pictures fill in later.
  • "display: none and visibility: hidden do the same thing." No. display: none leaves an element out of the render tree. visibility: hidden keeps it, as an empty box that takes up space.
  • "Inline all the CSS." No. Inlined CSS comes again with every page, and the browser can't cache it on its own. Inline only what the first screen needs.
  • "The Paint step's time is the first contentful paint." No. The paint entry marks when the frame was shown, where the browser knows that moment. On Asha's page, it came 14.4 ms after the Paint step.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upOne more hidden element

Leaf and Kettle adds a promo to the body, hidden with display: none. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// The elements in the Mint tea page's body.
const elements = ["body", "header", "main", "h1", "p", "p.note", "aside.promo"];
// The rules for .note and .promo both say display: none.
const hidden = ["p.note", "aside.promo"];

const renderTree = elements.filter((element) => !hidden.includes(element));
console.log(`The body has ${elements.length} elements in the DOM.`);
console.log(`Only ${renderTree.length} go into the render tree: ${renderTree.join(", ")}.`);

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

    Exercise 2 · Warm-upA later stylesheet

    The stylesheet now arrives 200 ms later. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // The same page: when the browser started reading each file, in ms.
    const readHtml = 168.7;
    const readStylesheet = 531.7;
    
    console.log(`The browser waited ${(readStylesheet - readHtml).toFixed(1)} ms for the stylesheet.`);

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

      Exercise 3 · PracticeA step only the script page has

      In the playground with four pages, pick A stylesheet and a script. One step in its list doesn't appear on the page with only a stylesheet. What is it called?

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

        Exercise 4 · PracticeThe work for 120 KB

        In the same playground, pick A 120 KB stylesheet. How long did the main thread's work take in all, in milliseconds?

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

          Exercise 5 · ChallengeFind the unused bytes

          The 120 KB stylesheet is mostly rules the page never uses. Which panel in Chrome's DevTools shows how many of a stylesheet's bytes went unused?

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

            Exercise 6 · ChallengeHow small is small?

            web.dev's article on critical CSS gives a size to keep the first screen's content under, to save round trips. What size is it?

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

              Check your understanding

              For each question, ask yourself: is this about the five steps, or about the path's files, round trips and bytes?

              The critical path quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the last line of the render tree example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The elements in the Mint tea page's body.
                const elements = ["body", "header", "main", "h1", "p", "p.note"];
                // The stylesheet's rule .note { display: none; } hides one of them.
                const hidden = ["p.note"];
                
                const renderTree = elements.filter((element) => !hidden.includes(element));
                console.log(`The body has ${elements.length} elements in the DOM.`);
                console.log(`Only ${renderTree.length} go into the render tree: ${renderTree.join(", ")}.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does the render tree combine?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the last line of the bytes example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The page with a small stylesheet, then with 120 KB of unused rules added to it.
                const small = { criticalBytes: 1324, firstPaint: 352 };
                const heavy = { criticalBytes: 121214, firstPaint: 544 };
                
                console.log(`Small stylesheet: ${small.criticalBytes} critical bytes, first paint at ${small.firstPaint} ms.`);
                console.log(`Heavy stylesheet: ${heavy.criticalBytes} critical bytes, first paint at ${heavy.firstPaint} ms.`);
                console.log(`The extra bytes pushed the first paint back ${heavy.firstPaint - small.firstPaint} ms.`);

                Choose an answer to see the explanation.

              4. Question 4 of 7In the recorded page with a stylesheet, where did most of the time before the first paint go?

                Choose an answer to see the explanation.

              5. Question 5 of 7What's the difference between first paint and first contentful paint?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why did adding a script to the page with a stylesheet barely change its first paint?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why shouldn't a big site inline all of its CSS in every page?

                Choose an answer to see the explanation.

              Key takeaways

              • The first paint needs the DOM and the CSSOM. The browser combines them into the render tree, then runs layout and paint.
              • display: none leaves an element out of the render tree. visibility: hidden keeps it, as an empty box.
              • On the Mint tea page, the five steps took about 5 ms. Waiting for the stylesheet took more than 30 times as long.
              • Measure the critical path with three numbers: its critical files, its round trips, and its critical bytes.
              • Files the browser finds together share a round trip. Their bytes still take time.
              • The paint entry's time comes a little after the Paint step. It's when the frame was shown, where the browser knows it.

              Remember the one-liner.
              The first paint waits for its critical files: fewer files, fewer round trips and fewer bytes paint sooner.

              Sources

              The facts on this page come from these docs and standards:

              Coming next: Reading a waterfall. The bars in this lesson's playground are a small waterfall. Next, you'll read every bar, color and gap of a real one, and spot the five most common problems.

              CompleteFrontend Clear concepts. Working examples.