completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

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.

By the end, you can
  • 01
    Tell 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.
  • 02
    Choose 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.
  • 03
    Let 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:

Three times from one visitPop out in the code editor (opens in a new tab)JavaScript
// 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. htmlDone is when the last byte of the HTML arrived. cssDone is when the stylesheet arrived. firstPaint is 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. and The 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 Mint tea page with a slow stylesheet, as bars. The HTML arrived from 0 ms to 156 ms. styles.css was asked for at 163 ms and arrived at 965 ms. DOMContentLoaded fired at 164 ms. The first paint came at 992 ms.the HTMLstyles.cssDOMContentLoaded, 164 msfirst paint, 992 ms0 ms
Shaded: the stylesheet. The browser had read the whole page by DOMContentLoaded, on the left. The screen stayed blank until the first paint, just after the stylesheet arrived.

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.

Real-life analogyA shop that waits for its window display

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.
The main idea

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:

  1. Why does a stylesheet hold back the paint, and when doesn't it?
  2. Why does a plain script stop the parser?
  3. What do async, defer and type="module" change?
  4. How can a script hold back the paint on purpose?
  5. Why is a chain of @import lines 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 paint follows the stylesheetPop out in the code editor (opens in a new tab)JavaScript
// 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 parser doesn't wait for CSSPop out in the code editor (opens in a new tab)JavaScript
// 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:

Two stylesheets, one only for printHTML
<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.css with media="print". It applies only when the page is printed.
Real-life analogyA sign for the printed flyers

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:

A slow stylesheet only for printPop out in the code editor (opens in a new tab)JavaScript
// 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.

Real-life analogyA "do this now" card in a recipe

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 parser waits for the scriptPop out in the code editor (opens in a new tab)JavaScript
// 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 paint waits tooPop out in the code editor (opens in a new tab)JavaScript
// 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. and With 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 can import other files. It downloads while the parser reads on. Like defer, it runs after the HTML is read.
Real-life analogyNotes for the cook

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.

When each kind of script loads and runs
The tagWhen 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:

Four rows, one for each kind of script. Classic: the parser parses, stops and waits for the script to download, runs it, then parses on. async: the parser keeps going while the script downloads, pauses to run it as soon as it arrives, then carries on. defer and module: the parser finishes, then the script runs.classicparsewait for scriptrunparsethe parser stops and waitsasyncparserunparseruns as soon as it arrivesdeferparserunruns after the HTML is readmoduleparserunruns like defer
Shaded: the script running. Only the classic script makes the parser stop and wait for the download. The other three download while the parser reads on.

The course loaded the same slow script all three ways. First, with async:

async: nothing on screen waitsPop out in the code editor (opens in a new tab)JavaScript
// 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. and The 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:

defer: DOMContentLoaded waitsPop out in the code editor (opens in a new tab)JavaScript
// 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:

type="module": like deferPop out in the code editor (opens in a new tab)JavaScript
// 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 async for 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:

Playground: ten real loads
The requests0 to 1.1 s
Fast CSS, no scriptrecorded
First contentful paint340 ms
DOMContentLoaded165 ms
load323 ms
Try it yourself
What held back the paint

Fast 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.

Ten real loads of the same page in Chrome. Every request had a 150 ms round trip at 20 Mbps, and in most loads the server held one file back 800 ms. Each bar is one file, from when the browser asked for it to when it arrived. The solid line is the first paint. The dashed line is DOMContentLoaded.

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.

blocking="render": the paint waitsPop out in the code editor (opens in a new tab)JavaScript
// 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. and blocking="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:

Playground: this page's render-blocking files
What Look does, shortenedJavaScript
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");
This pagenot read yet

Nothing read yet.

Try it yourself
Your page

Press Look. The panel lists this page's files that held back its first paint, and when each one arrived.

Read from your own browser. Resource Timing gives each file's renderBlockingStatus, and Paint Timing gives the first paint.

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.

Real-life analogyA treasure hunt

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 @import line
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:

Three stylesheets in a chainPop out in the code editor (opens in a new tab)JavaScript
// 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. and fonts.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 as link tagsPop out in the code editor (opens in a new tab)JavaScript
// 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. and fonts.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.

Two ways to load three stylesheets. Chained with @import: styles.css, then theme.css, then fonts.css, each found only when the one before arrives; the last arrived at 622 ms and the page painted at 640 ms. As three link tags: all three download at once, done by 319 ms; the page painted at 336 ms.@importstyles.csstheme.cssfonts.csspaint 640 msthree linksstyles.csstheme.cssfonts.csspaint 336 ms
Each @import is found only once the file before it has arrived. Link tags in the HTML are all found at once, so the three files download together.

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:

Does it block?
  • <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>
Try it yourself
0 of 8 correct

Sort each tag 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

  • "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.

Exercise 1 · Warm-upA slower script

The server now sends the classic script a whole second later, and the page paints at 1988 ms. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// 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.`);

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

    Exercise 2 · Warm-upA longer chain

    fonts.css now imports a fourth stylesheet, colors.css. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // 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.`);

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

      Exercise 3 · PracticeA print stylesheet that blocks

      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?

      Starter codeHTML
      <link rel="stylesheet" href="styles.css">
      <link rel="stylesheet" href="print.css">

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

        Exercise 4 · PracticeMenu first, then cart

        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?

        Starter codeHTML
        <script src="menu.js"></script>
        <script src="cart.js"></script>

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

          Exercise 5 · Challengeasync and the load event

          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?

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

            Exercise 6 · ChallengeThe other value

            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.

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

              Check your understanding

              For each question, ask: does this hold back the first paint, the parser, DOMContentLoaded, or nothing?

              The blocking quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the last line of the fast-and-slow stylesheet example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // 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.

              2. Question 2 of 7Why does a browser wait for a stylesheet before its first paint?

                Choose an answer to see the explanation.

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

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // 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.

              4. 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.

              5. Question 5 of 7What does blocking="render" do to an async script?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why is a chain of @imports slow?

                Choose an answer to see the explanation.

              7. 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, defer and module scripts download while the parser reads on. defer and module scripts make DOMContentLoaded wait.
              • A stylesheet whose media doesn't match still downloads, but the paint doesn't wait for it.
              • blocking="render" makes the paint wait on purpose. Firefox ignores it.
              • Chained @import lines 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:

              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.

              CompleteFrontend Clear concepts. Working examples.