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.
- 01Walk the critical rendering pathFollow the HTML into the DOM and the CSS into the CSSOM, then through the render tree, layout and paint.
- 02Measure 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.
- 03Shorten 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:
// 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.andThe 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.
- HTML to DOM. The browser reads the HTML and builds the DOM, a tree of the page's elements.
- CSS to CSSOM. It reads the CSS and builds the CSSOM, a tree of the style rules.
- The render tree. It puts the two together. The render tree holds every element that will be drawn, each with its styles.
- Layout. It works out where each element goes on the screen, and how big it is.
- Paint. It turns each element into pixels.
Here is the whole path in one picture:
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 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:
<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:
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:
- It walks the DOM from the top, one element at a time.
- For each element, it finds the CSS rules that match, and works out its final styles.
- 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:
// 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.notedisplay: 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:
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.
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:
// 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:
// 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.
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:
// 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.andCSS 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:
// 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.andWith 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."
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:
// 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.andHeavy 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:
21,324 bytes5.3 ms352 msA 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.
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.
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 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.
| What to cut | How | Why it helps |
|---|---|---|
| Fewer critical files | Put 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 trips | Link 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 bytes | Remove 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?
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:
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));221,324 bytes302 ms2 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.
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:
- 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
Sort each file by whether the first paint waits for it.
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: noneleaves an element out of the render tree.visibility: hiddenkeeps 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.
Leaf and Kettle adds a promo to the body, hidden with display: none. What does the last line print?
// 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(", ")}.`);Only 5 go into the render tree: body, header, main, h1, p. The DOM grew to 7 elements, but the render tree didn't change, because the promo has display: none.
The stylesheet now arrives 200 ms later. What does the last line print?
// 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.`);The browser waited 363.0 ms for the stylesheet. The five steps still take about 5 ms. Only the waiting grows, and the first paint moves back with it.
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?
Evaluate script: the browser running app.js. Look at the steps after it: the parser read the rest of the HTML only once the script had run, because a plain script in the head stops the parser.
In the same playground, pick A 120 KB stylesheet. How long did the main thread's work take in all, in milliseconds?
7.3 ms, 2 ms more than with the small stylesheet. The extra 120 KB took 192 ms to download, but only about 2 ms of the browser's work.
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?
Coverage. Press its reload button, and it lists every file with its used and unused bytes. Removing the unused CSS cuts the critical bytes.
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?
14 KB, compressed. A new connection sends about that much in its first round trip, so content that size can arrive without waiting for more trips.
Check your understanding
For each question, ask yourself: is this about the five steps, or about the path's files, round trips and bytes?
Question 1 of 7What does the last line of the render tree example print?
Read the code, then predict// 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.
Question 2 of 7What does the render tree combine?
Choose an answer to see the explanation.
Question 3 of 7What does the last line of the bytes example print?
Read the code, then predict// 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.
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.
Question 5 of 7What's the difference between first paint and first contentful paint?
Choose an answer to see the explanation.
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.
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: noneleaves an element out of the render tree.visibility: hiddenkeeps 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:
- Critical rendering path, MDN.
- Constructing the Object Model, Ilya Grigorik, web.dev.
- Render-tree construction, layout, and paint, Ilya Grigorik, web.dev.
- HTML Standard: hidden elements, WHATWG.
- Main thread, MDN.
- Analyzing critical rendering path performance, Ilya Grigorik, web.dev.
- Paint Timing, W3C.
- PerformancePaintTiming, MDN.
- Extract critical CSS, Milica Mihajlija, web.dev.
- Coverage: Find unused JavaScript and CSS, Chrome for Developers.
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.