Render-blocking and parser-blocking resources
CSS holds back the first paint and plain scripts stop the parser. See ten real loads of one page, and what async, defer, media and @import change.
- 01Tell the two kinds of waiting apartExplain why a stylesheet holds back the first paint, and why a plain script stops the browser reading the HTML.
- 02Choose how a script loadsPick a classic script, async, defer or type="module" by when each one downloads, when it runs, and what waits for it.
- 03Let the first paint come soonerUse media for print styles, link tags instead of @import chains, and blocking="render" only where the first paint needs it.
What holds back the first paint
Asha opens the Mint tea page at Leaf and Kettle, the tea shop from the first lessons. The page is small. All of its HTML reaches her phone in about a sixth of a second. Yet the screen stays blank for almost a whole second. The words are already there. So what is the browser waiting for?
The course loaded that page in Chrome with one change: the server sent the page's stylesheet 800 ms late. A stylesheet is the CSS file that says how the page looks: its fonts, its spacing, the green bar along the top. Here are three times Chrome wrote down on that visit:
// The Mint tea page. The server sent its stylesheet 800 ms late.// Every time is in milliseconds (ms) after the visit started.const visit = { htmlDone: 156, cssDone: 965, firstPaint: 992 }; 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. They say what was recorded. Every time is counted in milliseconds (ms) from the start of the visit.
- Line 3 holds three recorded times.
htmlDoneis when the last byte of the HTML arrived.cssDoneis when the stylesheet arrived.firstPaintis when the first words appeared on the screen. - Lines 5 to 7 print them:
All the HTML had arrived at 156 ms.,The stylesheet arrived at 965 ms.andThe first words appeared at 992 ms.
Look at the last two lines. The browser had every word by 156 ms. It showed none of them until 27 ms after the stylesheet came in. It was waiting for the stylesheet. One slow file kept Asha looking at a blank screen for most of a second.
Here is the same visit as bars. Each bar starts when the browser asked for a file, and ends when the file had arrived. Two upright lines mark two moments:
The left line is DOMContentLoaded, an event the browser fires once it has read all the HTML and built the page from it. It fired at 164 ms. The right line is the first paint, at 992 ms. So the page was read and ready more than 800 ms before anyone could see it.
At Leaf and Kettle, the shelves are stocked by eight in the morning. But the owner won't open the doors until the window display is finished. She doesn't want people to see a half-dressed window and think the shop is a mess. So customers wait outside, even though everything inside is ready.
- In real life: The shelves, stocked and ready
- In JavaScript: The HTML, downloaded and read by DOMContentLoaded
- In real life: The window display
- In JavaScript: The stylesheet
- In real life: Opening the doors
- In JavaScript: The first paint
- In real life: Customers waiting outside
- In JavaScript: Asha, looking at a blank screen
A file the browser must have before it paints anything is called render-blocking. To render a page is to draw it on the screen. The stylesheet above was render-blocking. It was the window display.
There's a second kind of waiting. The parser is the part of the browser that reads the HTML from top to bottom and builds the page from it. Reading HTML this way is called parsing. Some files make the parser itself stop and wait. They're called parser-blocking.
Both kinds matter most in the page's head. That's the top part of the HTML, before <body>, where a page lists its title, stylesheets and scripts. So a file in the head can hold back one of two things:
- Render-blocking: the parser reads on, but the browser won't paint until it has the file. A stylesheet in the head works this way.
- Parser-blocking: the parser stops reading until the file has downloaded and run. A plain
<script src>tag works this way. In the head, it holds back the paint too.
A stylesheet in the head holds back the paint. A plain script holds back the parser, and in the head, the paint too. The first paint can only come once the head is done waiting. So make the head wait only for what the first paint needs.
The course loaded the Mint tea page ten ways in Chrome. Each time, it changed one thing in the head. Every request had a 150 ms round trip, the slow network from lesson 4.1. In most loads, the server held one file back 800 ms. The ten loads answer five questions, one per section:
- Why does a stylesheet hold back the paint, and when doesn't it?
- Why does a plain script stop the parser?
- What do
async,deferandtype="module"change? - How can a script hold back the paint on purpose?
- Why is a chain of
@importlines slow?
For the same story from JavaScript's side, see Page & resource loading. For everything the first pixels wait for, see The critical rendering path.
Why CSS blocks rendering
Why would a browser wait for a stylesheet when it already has the words? Picture the page without one. The heading shows in the browser's plain default font, black on white. A moment later the stylesheet arrives, and everything jumps. The green bar appears, the fonts change, and the text moves. That jump is called a flash of unstyled content. Browsers avoid it by waiting.
The HTML standard, the document that says how browsers must behave, makes this a rule. A stylesheet the parser finds in the head blocks rendering, as long as its media matches the screen. That's the window display: the doors stay shut until it's done. You'll meet media at the end of this section.
How much does the wait cost? Here is the page with a fast stylesheet, and then with the slow one:
// The same page twice: with a fast stylesheet, then with a slow one.const fast = { cssDone: 317, firstPaint: 340 };const slow = { cssDone: 965, firstPaint: 992 }; console.log(`Fast stylesheet: the page painted ${fast.firstPaint - fast.cssDone} ms after it arrived.`);console.log(`Slow stylesheet: the page painted ${slow.firstPaint - slow.cssDone} ms after it arrived.`);console.log(`The slow stylesheet pushed the first paint back ${slow.firstPaint - fast.firstPaint} ms.`);- Line 1 is a comment. Lines 2 and 3 hold two recorded times for each load: when the stylesheet arrived, and when the page first painted.
- Line 5 prints
Fast stylesheet: the page painted 23 ms after it arrived. - Line 6 prints
Slow stylesheet: the page painted 27 ms after it arrived. - Line 7 compares the two paints. It prints
The slow stylesheet pushed the first paint back 652 ms.
Fast or slow, the paint came a few milliseconds after the stylesheet. The stylesheet set the time of the first paint. That's why every stylesheet in the head matters: if one is slow, the whole page is late.
The parser reads on
While the browser waits to paint, does it stop reading the HTML too? No. Here is the slow-stylesheet page again:
// The page with the slow stylesheet again.const visit = { domContentLoaded: 164, cssDone: 965 }; console.log(`DOMContentLoaded fired at ${visit.domContentLoaded} ms.`);console.log(`The stylesheet arrived ${visit.cssDone - visit.domContentLoaded} ms after that.`);- Line 2 holds two recorded times: when DOMContentLoaded fired, and when the stylesheet arrived.
- Line 4 prints
DOMContentLoaded fired at 164 ms. - Line 5 prints
The stylesheet arrived 801 ms after that.
The parser read the whole page while the stylesheet was still on its way. Only the paint waited. In the shop, the shelves were stocked on time. Only the doors stayed shut.
One thing does wait for a stylesheet: a plain script below it. A script can ask how big something on the page is, and the answer depends on the CSS. So the browser runs such a script only once the stylesheets above it have arrived.
Stylesheets that don't match the screen
Some CSS isn't for the screen at all. Leaf and Kettle has a print stylesheet. It hides the green bar and uses black text, so the page looks right on paper. The media attribute on a link tag says when its stylesheet applies:
<link rel="stylesheet" href="styles.css"><link rel="stylesheet" href="print.css" media="print">- Line 1 links the main stylesheet. It has no
media, so it applies everywhere. - Line 2 links
print.csswithmedia="print". It applies only when the page is printed.
The shop is also making a sign that will only go on its printed flyers. It will never stand in the window. So the owner doesn't wait for it. The doors open as soon as the window display is done, and the flyer sign turns up whenever it's ready.
- In real life: The window display
- In JavaScript: A stylesheet whose media matches the screen
- In real life: The sign for the printed flyers
- In JavaScript: A stylesheet with media="print"
- In real life: The owner opening the doors without it
- In JavaScript: The browser painting without waiting for it
- In real life: The sign turning up later, for the flyers
- In JavaScript: The print stylesheet still downloading
The browser checks media before it waits. On a screen, print doesn't match, so this stylesheet can't change what Asha sees. The browser still downloads it, in case she prints the page. But it doesn't wait for it. The course made print.css the slow file:
// The same slow stylesheet, now marked media="print".const visit = { printCssDone: 965, firstPaint: 332 }; console.log(`The page painted at ${visit.firstPaint} ms.`);console.log(`The print stylesheet arrived at ${visit.printCssDone} ms, long after.`);- Line 2 holds two recorded times: when the print stylesheet arrived, and when the page first painted.
- Line 4 prints
The page painted at 332 ms. - Line 5 prints
The print stylesheet arrived at 965 ms, long after.
The same 800 ms delay that held the paint back before held back nothing here. The condition inside media is called a media query. It can be print, or a screen size such as (min-width: 600px). Any stylesheet that's only for print, or only for some screens, can carry one. Then it stops holding back the first paint everywhere else.
Classic scripts block the parser
Now the second kind of waiting. A classic script is a plain script tag that loads a file, like <script src="widget.js">, with no async, defer or type="module". When the parser meets one, it stops. It waits for the file to download, runs it, and only then reads on.
Why stop? Because a script can change the HTML that comes after it. An old method, document.write(), writes new HTML into the page right where the script sits. If the parser had read ahead and built the rest of the page, that work could be wrong. So it waits.
A cook works through a recipe from top to bottom. Halfway down, there's a card: "Do this now, before you read on." It needs a sauce from the shop next door. So the cook stops, waits for the sauce, makes what the card asks, and only then reads the next line. The card might even cross out a step below it, so reading ahead isn't safe.
- In real life: The cook reading the recipe, line by line
- In JavaScript: The parser reading the HTML
- In real life: The "do this now" card
- In JavaScript: A classic script tag
- In real life: Waiting for the sauce from the shop next door
- In JavaScript: Downloading the script
- In real life: Making what the card asks
- In JavaScript: Running the script
- In real life: Reading the next line
- In JavaScript: The parser going on
The course put a classic script in the head of the Mint tea page, and had the server send it 800 ms late:
// The page with a classic script in its head. The server sent the script 800 ms late.const visit = { scriptDone: 966, domContentLoaded: 967 }; console.log(`DOMContentLoaded fired ${visit.domContentLoaded - visit.scriptDone} ms after the script arrived.`);- Line 1 is a comment. Line 2 holds two recorded times: when the script arrived, and when DOMContentLoaded fired.
- Line 4 subtracts one from the other. It prints
DOMContentLoaded fired 1 ms after the script arrived.
Without the script, DOMContentLoaded fired at 165 ms. With it, the parser sat at the script tag for about 800 ms. Once the script had arrived and run, the parser finished the page within a millisecond. That's the cook, waiting for the sauce.
The script was in the head, so the paint waited too. The HTML standard counts a classic script in the head as render-blocking. And there's nothing to paint yet anyway: the parser hasn't reached the body. Here is the first paint without the script and with it:
// The first paint, without the classic script and with it.const noScript = { firstPaint: 340 };const classic = { firstPaint: 988 }; console.log(`Without the script, the page painted at ${noScript.firstPaint} ms.`);console.log(`With it, the page painted at ${classic.firstPaint} ms.`);console.log(`The script held the paint back ${classic.firstPaint - noScript.firstPaint} ms.`);- Lines 2 and 3 hold the recorded first paint: without the script, and with it.
- Lines 5 and 6 print
Without the script, the page painted at 340 ms.andWith it, the page painted at 988 ms. - Line 7 subtracts. It prints
The script held the paint back 648 ms.
A classic script in the head passes most of its delay on to the screen. One slow script from another company, like an advertising tool, can make every visitor wait for it. While the parser waits, the preload scanner from lesson 5.2 reads ahead and starts downloading the files it finds. But only the parser can build the page.
This is why old advice says to put scripts at the end of the body. By the time the parser stops there, the page above the script is built, and the browser can paint it. The next section has a better way, which starts the download right away, from the head.
async, defer, and type=module
Most scripts don't need to stop the parser. They don't call document.write(), and they don't need to run before the rest of the page is read. You can tell the browser so with one word in the tag. There are three choices:
async: download while the parser reads on, and run as soon as the file arrives.defer: download while the parser reads on, and run once the whole HTML is read, in the order the tags appear.type="module": the script is a JavaScript module, a file that canimportother files. It downloads while the parser reads on. Likedefer, it runs after the HTML is read.
The same cook gets two other kinds of note. One says "Do this whenever you're free." The cook keeps reading, and does it the moment its ingredients turn up, even between two steps. A pile of others says "Do these after the main course, in this order." The cook finishes the recipe first, then works through the pile from the top. Only then does the cook call everyone to the table.
- In real life: "Do this whenever you're free"
- In JavaScript:
async: run as soon as the file arrives - In real life: Doing it between two steps, the moment it turns up
- In JavaScript: An async script pausing the parser to run
- In real life: "Do these after the main course, in this order"
- In JavaScript:
defer: run after the HTML is read, in order - In real life: Calling everyone to the table once the pile is done
- In JavaScript: DOMContentLoaded, which waits for deferred scripts
The HTML standard defines when each kind runs. Read the table across: each row is one kind of script tag.
| The tag | When does it download? | When does it run? | What waits for it? |
|---|---|---|---|
| <script src> | While the parser waits for it. | As soon as it arrives. Then the parser reads on. | The parser. In the head, the first paint too. |
| <script async> | While the parser reads on. | As soon as it arrives, even halfway through the HTML. | Not the paint, and not DOMContentLoaded. |
| <script defer> | While the parser reads on. | After the whole HTML is read, in the order the tags appear. | DOMContentLoaded, but not the paint. |
| <script type=module> | While the parser reads on, with the files it imports. | Like defer. With async added, like async. | DOMContentLoaded, like defer. |
The picture shows the same four rows over time. "Parse" is the parser reading the HTML:
The course loaded the same slow script all three ways. First, with async:
// The same slow script, loaded with async.const visit = { domContentLoaded: 163, firstPaint: 332, scriptDone: 965 }; console.log(`DOMContentLoaded fired at ${visit.domContentLoaded} ms.`);console.log(`The page painted at ${visit.firstPaint} ms.`);console.log(`The script arrived at ${visit.scriptDone} ms, after both.`);- Line 2 holds three recorded times.
- Lines 4 and 5 print
DOMContentLoaded fired at 163 ms.andThe page painted at 332 ms. - Line 6 prints
The script arrived at 965 ms, after both.
Both came as early as on the page with no script at all. The parser read on, and the script ran when it turned up. That's the "whenever you're free" note.
Next, with defer:
// The same slow script, loaded with defer.const visit = { firstPaint: 336, scriptDone: 968, domContentLoaded: 969 }; console.log(`The page painted at ${visit.firstPaint} ms.`);console.log(`The script arrived at ${visit.scriptDone} ms.`);console.log(`DOMContentLoaded waited for it, and fired at ${visit.domContentLoaded} ms.`);- Line 2 holds three recorded times, in the order they happened.
- Line 4 prints
The page painted at 336 ms. - Line 5 prints
The script arrived at 968 ms. - Line 6 prints
DOMContentLoaded waited for it, and fired at 969 ms.
The paint didn't wait. DOMContentLoaded did, because deferred scripts run after the HTML is read but before DOMContentLoaded fires. That's the pile of notes: nobody is called to the table until it's done. So DOMContentLoaded really means two things: the HTML is read, and the deferred scripts have run. Any code that waits for it also waits for them.
Last, as a module:
// The same slow script, loaded with type="module".const visit = { firstPaint: 332, domContentLoaded: 965 }; console.log(`The page painted at ${visit.firstPaint} ms.`);console.log(`DOMContentLoaded waited for the script, and fired at ${visit.domContentLoaded} ms.`);- Line 2 holds two recorded times: the first paint, and DOMContentLoaded.
- Line 4 prints
The page painted at 332 ms. - Line 5 prints
DOMContentLoaded waited for the script, and fired at 965 ms.
That's the same as defer. A module script is deferred unless you add async. So which one should you use?
- Use
defer, or a module, for a script that needs the whole page, or needs another script to run first. - Use
asyncfor a script that stands alone and doesn't care about order, like a visitor counter. - Keep a classic script only if it truly must run before the parser reads on.
Now try all ten loads yourself. Pick one, and watch whether the solid line, the first paint, waits for the slow file's bar:
340 ms165 ms323 msFast CSS, no script: first paint at 340 ms, DOMContentLoaded at 165 ms. Nothing is slow here. The page painted as soon as its stylesheet arrived.
blocking=render
Sometimes the first paint really does need a script. Say Leaf and Kettle adds a dark theme, and a small script picks the theme each visitor chose last time. If the page painted before the script ran, Asha would see a flash of the light theme, then the dark one.
A classic script would stop that flash, but it would also stop the parser. The blocking="render" attribute asks for only the part you need: the paint waits for the script, and the parser reads on. In the shop, it adds one more job to the window display. The doors now wait for it too.
// The same async script, without and with blocking="render".const plainAsync = { firstPaint: 332 };const blockingRender = { scriptDone: 965, firstPaint: 992 }; console.log(`Plain async: the page painted at ${plainAsync.firstPaint} ms.`);console.log(`blocking="render": the script arrived at ${blockingRender.scriptDone} ms.`);console.log(`blocking="render": the page painted at ${blockingRender.firstPaint} ms.`);- Line 1 is a comment. Line 2 holds the recorded first paint with plain
async. - Line 3 holds the same script with
blocking="render": when it arrived, and when the page painted. - Line 5 prints
Plain async: the page painted at 332 ms. - Lines 6 and 7 print
blocking="render": the script arrived at 965 ms.andblocking="render": the page painted at 992 ms.
The paint now waited for the script, 660 ms longer than with plain async. DOMContentLoaded still fired at 165 ms, so the parser didn't wait. Use it only for something the first paint truly needs, because every visitor waits for it. Firefox ignores the attribute and paints without waiting.
Which files held back this page?
Chrome can tell you which files held back a page's first paint. The browser keeps a list of every file a page loaded, with times, called Resource Timing. In Chrome, each file in it has a renderBlockingStatus. Press Look to read this page's own files:
const files = performance.getEntriesByType("resource");const [paint] = performance.getEntriesByName("first-contentful-paint"); const blocking = files.filter((file) => file.renderBlockingStatus === "blocking");for (const file of blocking) console.log(file.name, Math.round(file.responseEnd), "ms");console.log("first contentful paint:", Math.round(paint.startTime), "ms");Nothing read yet.
Press Look. The panel lists this page's files that held back its first paint, and when each one arrived.
Line 4 of the panel's code keeps only the files Chrome marked "blocking". Line 5 prints when each one arrived.
@import chains
A stylesheet can pull in another stylesheet with an @import line at its top. It's a handy way to split CSS into files. But it hides the second file from the browser. The browser only learns about theme.css once styles.css has arrived and been read. If theme.css imports fonts.css, that one is found later still.
In a treasure hunt, each clue tells you where the next one is hidden. You can't walk to the third spot until you've read the second clue. If all three spots were written on one card, three friends could fetch them at the same time.
- In real life: Each clue
- In JavaScript: A stylesheet
- In real life: "The next clue is under the bench"
- In JavaScript: An
@importline - In real life: Walking to each spot
- In JavaScript: One round trip to the server
- In real life: One card that lists every spot
- In JavaScript: A link tag for each stylesheet in the HTML
The course built that chain: styles.css imports theme.css, which imports fonts.css. The files are tiny, so almost all of each download is the 150 ms round trip:
// styles.css imports theme.css, and theme.css imports fonts.css.// Each pair is [asked for at, arrived at], in ms.const chain = { "styles.css": [162, 315], "theme.css": [315, 470], "fonts.css": [470, 622] }; for (const [file, [askedAt, arrivedAt]] of Object.entries(chain)) { console.log(`${file}: asked for at ${askedAt} ms, arrived at ${arrivedAt} ms.`);}- Lines 1 and 2 are comments. They say what imports what, and what each pair of numbers means.
- Line 3 holds the recorded pair for each file.
- Lines 5 to 7 loop over the files and print one line for each.
- It prints
styles.css: asked for at 162 ms, arrived at 315 ms.,theme.css: asked for at 315 ms, arrived at 470 ms.andfonts.css: asked for at 470 ms, arrived at 622 ms.
Each file was asked for the moment the one before it arrived. That's three round trips, one after another. Here are the same three files, each with its own link tag in the HTML:
// The same three stylesheets, each with its own <link> tag in the HTML.// Each pair is [asked for at, arrived at], in ms.const links = { "styles.css": [163, 318], "theme.css": [163, 318], "fonts.css": [164, 319] }; for (const [file, [askedAt, arrivedAt]] of Object.entries(links)) { console.log(`${file}: asked for at ${askedAt} ms, arrived at ${arrivedAt} ms.`);}- Line 3 holds the recorded pairs.
- It prints
styles.css: asked for at 163 ms, arrived at 318 ms.,theme.css: asked for at 163 ms, arrived at 318 ms.andfonts.css: asked for at 164 ms, arrived at 319 ms.
All three were asked for at once, so all three arrived at once: one round trip instead of three. The preload scanner found the three link tags in the HTML. It never reads CSS, so it couldn't see the imports.
The first paint followed the CSS: 640 ms with the chain, 336 ms with the links. Every @import in a blocking stylesheet adds a round trip before the first paint. So list each stylesheet in the HTML instead.
Does it block?
Here are eight tags you might find in a real page's head. For each one, ask a single question: does the first paint have to wait for it? Sort them:
- <link rel="stylesheet" href="main.css"> in the head
- <script src="ab-test.js"> in the head
- <script src="theme.js" async blocking="render">
- A stylesheet the main stylesheet @imports
- <link rel="stylesheet" href="print.css" media="print">
- <script src="app.js" defer>
- <script src="app.js" type="module">
- <script src="chat.js" async>
Sort each tag by whether the first paint waits for it.
Common mix-ups
- "CSS stops the parser." No. It holds back the paint. The parser reads on. With the slow stylesheet, DOMContentLoaded fired at 164 ms, 801 ms before the stylesheet arrived.
- "async and defer are the same." No. An async script runs as soon as it arrives, in any order. A deferred script runs after the HTML is read, in the order of the tags.
- "defer makes a script free." No. The paint doesn't wait for it, but DOMContentLoaded does. So does any code that waits for DOMContentLoaded.
- "Module scripts block like classic ones." No. A module script is deferred unless it has
async. - "@import is as fast as link tags." No. Each import is found only after the file before it arrives. The chain of three was in at 622 ms. The three link tags were in at 319 ms.
- "A print stylesheet slows the first paint." No. With
media="print", it downloads, but the paint doesn't wait for it.
Practice exercises
Answer first, then use the hints.
The server now sends the classic script a whole second later, and the page paints at 1988 ms. What does the last line print?
// The server now sends the classic script a whole second later.
const noScript = { firstPaint: 340 };
const classic = { firstPaint: 1988 };
console.log(`Without the script, the page painted at ${noScript.firstPaint} ms.`);
console.log(`With it, the page painted at ${classic.firstPaint} ms.`);
console.log(`The script held the paint back ${classic.firstPaint - noScript.firstPaint} ms.`);The script held the paint back 1648 ms. The whole extra second went to the paint, because a classic script in the head holds the paint until it has run.
fonts.css now imports a fourth stylesheet, colors.css. What does the last line print?
// fonts.css now imports a fourth stylesheet, colors.css.
const chain = { "styles.css": [162, 315], "theme.css": [315, 470], "fonts.css": [470, 622], "colors.css": [622, 775] };
const lastArrival = Math.max(...Object.values(chain).map(([, arrivedAt]) => arrivedAt));
console.log(`All the CSS was in at ${lastArrival} ms.`);All the CSS was in at 775 ms. Each extra @import adds about one more round trip to the chain.
Leaf and Kettle links its print styles like this. Now every visitor waits for print.css before the first paint. What do you add to line 2 to fix it?
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css"><link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="print.css" media="print">media="print". On a screen, print doesn't match, so the browser downloads print.css without waiting for it. When someone prints the page, it applies.
The head has two classic scripts. cart.js uses a function from menu.js, so menu.js must run first. Which attribute do you add to both, so the paint stops waiting and the order stays the same?
<script src="menu.js"></script>
<script src="cart.js"></script><script src="menu.js" defer></script>
<script src="cart.js" defer></script>defer. Both scripts download while the parser reads on, so the paint doesn't wait. They run after the HTML is read, in the order of the tags, so menu.js runs first. type="module" works the same way.
In the ten-loads playground, pick async script. Neither the paint nor DOMContentLoaded waited for the script. When did the load event fire, in milliseconds?
966 ms, 1 ms after the script arrived. The load event waits for every file the page asked for, async scripts too. So async frees the paint and DOMContentLoaded, but not the load event.
In the panel above, Look keeps the files whose renderBlockingStatus is "blocking". What value does Chrome give a file the paint didn't wait for? Look it up on MDN.
non-blocking. Chrome gives it to every file the first paint didn't wait for, like an async script or a print stylesheet.
Check your understanding
For each question, ask: does this hold back the first paint, the parser, DOMContentLoaded, or nothing?
Question 1 of 7What does the last line of the fast-and-slow stylesheet example print?
Read the code, then predict// The same page twice: with a fast stylesheet, then with a slow one. const fast = { cssDone: 317, firstPaint: 340 }; const slow = { cssDone: 965, firstPaint: 992 }; console.log(`Fast stylesheet: the page painted ${fast.firstPaint - fast.cssDone} ms after it arrived.`); console.log(`Slow stylesheet: the page painted ${slow.firstPaint - slow.cssDone} ms after it arrived.`); console.log(`The slow stylesheet pushed the first paint back ${slow.firstPaint - fast.firstPaint} ms.`);Choose an answer to see the explanation.
Question 2 of 7Why does a browser wait for a stylesheet before its first paint?
Choose an answer to see the explanation.
Question 3 of 7What does the last line of the defer example print?
Read the code, then predict// The same slow script, loaded with defer. const visit = { firstPaint: 336, scriptDone: 968, domContentLoaded: 969 }; console.log(`The page painted at ${visit.firstPaint} ms.`); console.log(`The script arrived at ${visit.scriptDone} ms.`); console.log(`DOMContentLoaded waited for it, and fired at ${visit.domContentLoaded} ms.`);Choose an answer to see the explanation.
Question 4 of 7On the recorded page with a slow stylesheet and no script, did DOMContentLoaded wait for the stylesheet?
Choose an answer to see the explanation.
Question 5 of 7What does blocking="render" do to an async script?
Choose an answer to see the explanation.
Question 6 of 7Why is a chain of @imports slow?
Choose an answer to see the explanation.
Question 7 of 7When does a module script without async run?
Choose an answer to see the explanation.
Key takeaways
- A stylesheet in the head holds back the first paint, but the parser reads on.
- A classic script stops the parser until it has downloaded and run. In the head, it holds back the paint too.
async,deferand module scripts download while the parser reads on.deferand module scripts make DOMContentLoaded wait.- A stylesheet whose
mediadoesn't match still downloads, but the paint doesn't wait for it. blocking="render"makes the paint wait on purpose. Firefox ignores it.- Chained
@importlines download one file at a time. Link each stylesheet in the HTML instead.
Remember the one-liner.
CSS holds the paint, classic scripts hold the parser, and defer holds only DOMContentLoaded.
Sources
The facts on this page come from these standards and docs:
- The link element: the stylesheet keyword, HTML Living Standard, WHATWG.
- Render-blocking mechanism, HTML Living Standard, WHATWG.
- The script element, HTML Living Standard, WHATWG.
- Blocking attributes, HTML Living Standard, WHATWG.
- Eliminate render-blocking resources, Chrome for Developers.
- @import, MDN.
- PerformanceResourceTiming: renderBlockingStatus, MDN.
Coming next: How browsers prioritize requests, also in How the browser loads and renders. The browser gives every request a priority. Chrome, Firefox and Safari choose them differently, and fetchpriority lets you raise or lower them.