completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

HTML parsing and the preload scanner

The browser reads ahead in the HTML to fetch files early. See six real loads of one page, and what hides its main picture from the preload scanner.

By the end, you can
  • 01
    Explain the two readersSay how the parser reads HTML a piece at a time, why a plain script stops it, and how the preload scanner skims ahead to fetch files early.
  • 02
    Spot hidden filesRecognize files the scanner can't see: CSS backgrounds, tags added by scripts, lazy loading with data-src, and pages rendered on the client.
  • 03
    Check and fix discoveryRead when each file was asked for and what asked for it, then move its name into the HTML or preload it.

Two readers of the same HTML

In the last lesson, you followed one visit to completefrontend.com, from pressing Enter to the first paint. Near the end, the HTML started to arrive, and the browser began to read it. This lesson looks closely at that reading.

An HTML page is more than its own text. It names other files the page needs: a stylesheet, scripts, pictures. Each one is another request to the server, and each request takes time. So when does the browser ask for those files? Does it wait until its reading reaches each tag? Or does it ask sooner?

To find out, the course built a tiny page: the mint tea page at Leaf and Kettle, the tea shop from the first lessons. Its HTML is under 1 KB. It names a stylesheet and one big picture, a glass of mint tea of about 100 KB. The course loaded it in Chrome with a 150 ms round trip added to every request. A round trip is a message to the server and its reply back.

Here is the plain version, with the picture written in the HTML:

When the plain page asked for its picturePop out in the code editor (opens in a new tab)JavaScript
// The mint tea page, with the picture written in its HTML.// Every time is in milliseconds (ms) after the visit started.const load = { htmlArrived: 153, pictureAsked: 160, picturePainted: 392 }; console.log(`The last byte of the HTML arrived at ${load.htmlArrived} ms.`);console.log(`The browser asked for the picture at ${load.pictureAsked} ms.`);console.log(`The picture appeared on the screen at ${load.picturePainted} ms.`);
  • Lines 1 and 2 are comments. Every time is counted in milliseconds (ms) from the moment the visit started.
  • Line 3 holds three times Chrome recorded.
  • Lines 5 and 6 print The last byte of the HTML arrived at 153 ms. and The browser asked for the picture at 160 ms.
  • Line 7 prints The picture appeared on the screen at 392 ms.

The browser asked for the picture just 7 ms after the HTML arrived. The moment the picture appears is this page's LCP, Largest Contentful Paint: the moment the biggest thing on the screen is painted. Here, that's the picture.

Then the course wrote the same page five more ways. The picture, the network and the server stayed the same. Each version changed one thing: where the picture's name is written, or what comes before it. Here is when each version asked for the picture:

When each of the six pages asked for the picture, and when it arrived. In the HTML: asked at 160 ms, arrived at 356 ms; Behind a blocking script: asked at 160 ms, arrived at 357 ms; Added by a script: asked at 324 ms, arrived at 519 ms; A CSS background: asked at 318 ms, arrived at 514 ms; Rendered on the client: asked at 475 ms, arrived at 670 ms; Client-rendered, preloaded: asked at 164 ms, arrived at 362 ms. The HTML arrived at about 153 ms.In the HTMLasked at 160 msBehind a blocking scriptasked at 160 msAdded by a scriptasked at 324 msA CSS backgroundasked at 318 msRendered on the clientasked at 475 msClient-rendered, preloadedasked at 164 msthe HTML arrived, 153 ms0 ms
Each bar is the picture's download, from the moment the browser asked for it to its last byte. Shaded: the pages whose HTML names the picture. They asked for it as soon as the HTML arrived.

Three versions asked for the picture right after the HTML arrived. The other three asked 150 ms or more later. The version rendered on the client waited until 475 ms. Same picture, same network. Why?

The answer is that two readers go through the same HTML at the same time:

  • The HTML parser reads the HTML in order, from top to bottom. As it reads, it builds the page's tree of elements, the DOM. Some scripts make it stop and wait.
  • The preload scanner skims ahead in the same HTML. It looks only for the names of files. When it finds one, the browser starts fetching that file at once.
Real-life analogyA cook and a helper

A cook works through a recipe one step at a time: chop, stir, bake. While the cook reads the first step, a helper skims the whole page. The helper fetches every ingredient the recipe names and puts it on the counter. When the cook reaches the step that needs the mint, the mint is already there.

In real life: The cook, following the steps in order
In JavaScript: The HTML parser, building the DOM tag by tag
In real life: The helper, skimming the whole recipe
In JavaScript: The preload scanner
In real life: An ingredient named in the recipe
In JavaScript: A file named in the HTML, like <img src="mint.svg">
In real life: The mint, already on the counter
In JavaScript: The picture, downloaded before the parser reaches its tag
The main idea

The preload scanner can only fetch files whose names are written in the HTML. A file named anywhere else is found late: in the CSS, by a script, or in data. So if a file matters for the first view, write its name in the HTML.

The rest of the lesson takes the six versions one at a time. First: how the parser reads, and what stops it. The JavaScript course tells the same story from the script's side, in From HTML to running script.

Incremental HTML parsing

The parser doesn't wait for the whole file. It starts with the first bytes that arrive, builds what it can, and goes on as more come in. That's what incremental means: a piece at a time. It's like reading a long letter page by page, as each page is handed to you. On a big page, the browser can paint the top while the bottom is still downloading.

A script that makes the parser stop

One kind of tag makes the parser stop: a <script src> without async or defer. The parser can't read past it until the script has downloaded and run.

Real-life analogyA "do this first" card

Halfway down the recipe, the cook finds a card: "Before you go on, make the sauce on the back." The cook can't skip it, because the sauce might change the next steps. So the cook stops, follows the card, and only then reads on. If the sauce needs something from the shop, the cook waits for the shop too.

In real life: The "do this first" card
In JavaScript: A <script src> without async or defer
In real life: The sauce might change the next steps
In JavaScript: The script can change the page before the parser reads on
In real life: Waiting for the shop
In JavaScript: Waiting for the script to download
In real life: Reading on once the sauce is made
In JavaScript: The parser going on once the script has run

Why can't the parser skip the script? Because a script can change the page while it's being read. It can even write new HTML into the spot where it sits, with document.write(). So the parser has to let it run first. A script that stops the parser like this is called parser-blocking.

The words async and defer let the parser go on while the script downloads. The next lesson, Render-blocking and parser-blocking resources, covers them.

The course tried a parser-blocking script on the tea page. It added widget.js to the head, a stand-in for a slow third-party script. A third party is another company's code that a page loads, like a chat box or an ad. The course made the server take a whole second to send it. Here is the top of that page:

The page with widget.js, shortenedHTML
<head>  <link rel="stylesheet" href="styles.css">  <script src="widget.js"></script></head><body>  <header>Leaf and Kettle</header>  <main id="tea">    <img id="hero" src="mint.svg" alt="A glass of mint tea" width="360" height="270">
  • Line 2 names the stylesheet.
  • Line 3 is the script. It has no async or defer, so the parser stops here.
  • Line 8 is the picture, five lines further down.

How much did the script cost? Here are two recorded loads, one with the script and one without it:

What the blocking script costPop out in the code editor (opens in a new tab)JavaScript
// The tea page with widget.js in its head, and the same page without it.const withTheScript = { scriptArrived: 1164, picturePainted: 1192 };const withoutIt = { picturePainted: 392 }; console.log(`The parser waited for widget.js until ${withTheScript.scriptArrived} ms.`);console.log(`The picture appeared ${withTheScript.picturePainted - withoutIt.picturePainted} ms later than without the script.`);
  • Line 2 holds two times from the page with the script: when widget.js arrived, and when the picture appeared.
  • Line 3 holds when the picture appeared on the page without it.
  • Line 5 prints The parser waited for widget.js until 1164 ms.
  • Line 6 subtracts one paint time from the other. It prints The picture appeared 800 ms later than without the script.

The parser sat at line 3 for about a second. Nothing at all was painted until the script had arrived. One slow script in the head held back the whole page.

But what about the picture on line 8? Did the browser wait a whole second to ask for it too?

What the preload scanner finds

It didn't wait. Here is the picture's download on the same page:

The picture didn't wait for the parserPop out in the code editor (opens in a new tab)JavaScript
// The page with widget.js again: the picture's download.const picture = { askedAt: 160, arrivedAt: 357 };const scriptArrived = 1164; console.log(`The picture was asked for at ${picture.askedAt} ms.`);console.log(`It had fully arrived at ${picture.arrivedAt} ms.`);console.log(`That was ${scriptArrived - picture.arrivedAt} ms before widget.js arrived and let the parser go on.`);
  • Line 2 holds when the browser asked for the picture, and when its last byte arrived.
  • Line 3 holds when widget.js arrived.
  • Lines 5 and 6 print The picture was asked for at 160 ms. and It had fully arrived at 357 ms.
  • Line 7 prints That was 807 ms before widget.js arrived and let the parser go on.

160 ms is exactly when the plain page asked for its picture. The parser was stuck at the script. Yet something read past it, found the <img> tag, and asked for the picture. That something was the preload scanner: the helper who skims the recipe while the cook is busy.

Two readers of the same HTML. The parser reaches the script tag and waits for widget.js, which arrived at 1,164 ms in the recording. Meanwhile the preload scanner reads ahead, finds the img tag and fetches mint.svg, which finished at 357 ms.parser<link css><script>waits until 1,164 ms<img>preload scanner<link css><script><img>mint.svgdone at 357 ms
The parser stops at the script and waits. The preload scanner reads on, finds the <img> tag and fetches the picture. By the time the parser gets there, the picture has arrived.

The HTML standard calls this speculative parsing. Speculative means done ahead of time, on a guess. The browser fetches the files now, guessing that the page will still need them once the script has run. If the script changes the page so that a file isn't needed after all, that download is wasted.

Jeremy Wagner's web.dev article calls the preload scanner "a secondary HTML parser". It doesn't build anything. It only looks for file names. Read each row of this table as: when the HTML has this tag, the scanner does this.

What the preload scanner finds
When the HTML hasWhat the scanner does
<img src> and <img srcset>It fetches the picture the tag names.
<script src>, with or without async and deferIt fetches the script, even while another script holds up the parser.
<link rel=stylesheet>It fetches the stylesheet.
<link rel=preload>It fetches whatever the tag names, such as a font or a picture.

On the page with widget.js, the scanner found all three files at once. The stylesheet, the script and the picture were all asked for at 160 ms.

The standard leaves the exact list of tags to each browser. But one rule matters most. The preload scanner only reads HTML. It doesn't read CSS, and it doesn't run JavaScript.

Remember what the script cost, though. The scanner saved the picture's download. It couldn't save the paint, because only the parser builds the page, and the parser was still waiting. So a parser-blocking script costs time even when the scanner has fetched everything else.

Resources it can't see: CSS backgrounds, script-injected tags

The scanner only reads HTML. So what happens to a file whose name isn't in the HTML? Two of the six versions tried it.

Real-life analogyA note inside a jar

The recipe says: "Add the spice mix from the blue jar." The helper sees the blue jar in the recipe and fetches it. But inside the jar is a note: "This mix needs saffron." Nobody knows about the saffron until the jar is open. So someone makes a second trip to the shop, after the jar has arrived.

In real life: The recipe
In JavaScript: The HTML
In real life: The blue jar
In JavaScript: A file the HTML names, like styles.css or hero.js
In real life: The note inside the jar
In JavaScript: A picture named inside that file
In real life: The second trip to the shop
In JavaScript: A second request, which only starts once the first file has arrived

A picture set in the CSS

In the first of these versions, the picture is the background of a box. The HTML only has an empty <div class="hero">. The picture's name is in the stylesheet:

The picture's name, inside styles.cssCSS
/* styles.css: the picture is the background of the .hero box */.hero { background: url(mint.svg) center / cover; }

Line 1 is a comment. Line 2 gives the box a background picture with url(mint.svg). That's the only place the name is written.

The preload scanner never sees line 2. The browser learns the name only after styles.css has arrived and been read. Here is when that happened:

The background picture waited for the CSSPop out in the code editor (opens in a new tab)JavaScript
// The picture as a CSS background. Its name is only in styles.css.const background = { stylesArrived: 317, pictureAsked: 318 };const inTheHtml = { pictureAsked: 160 }; console.log(`styles.css arrived at ${background.stylesArrived} ms.`);console.log(`The picture was asked for at ${background.pictureAsked} ms, ${background.pictureAsked - inTheHtml.pictureAsked} ms later than from the HTML.`);
  • Line 2 holds two recorded times: when styles.css arrived, and when the browser asked for the picture.
  • Line 3 holds when the plain page asked for its picture.
  • Line 5 prints styles.css arrived at 317 ms.
  • Line 6 prints The picture was asked for at 318 ms, 158 ms later than from the HTML.

The browser asked for the picture 1 ms after the stylesheet arrived. It couldn't have asked any sooner: that's when it learned the name.

A picture added by a script

In the second version, the HTML has no <img> tag at all. A small script, hero.js, adds it. Here is the whole script:

hero.js, the script that adds the pictureJavaScript
// Add the picture once this script runs.document.querySelector("#tea").insertAdjacentHTML("afterbegin", '<img id="hero" src="mint.svg" alt="A glass of mint tea" width="360" height="270">');

Line 1 is a comment. Line 2 finds the box with the id tea. Then insertAdjacentHTML adds an <img> tag at its start, from a string of text. The name mint.svg lives only inside that string.

The browser finds hero.js at once, because its tag is in the HTML. But nothing runs it until it has arrived. The picture's tag only exists after that:

The added picture waited for the scriptPop out in the code editor (opens in a new tab)JavaScript
// The picture added by hero.js. Its name is only in the script.const injected = { scriptArrived: 317, pictureAsked: 324 };const inTheHtml = { pictureAsked: 160 }; console.log(`hero.js arrived at ${injected.scriptArrived} ms.`);console.log(`The picture was asked for at ${injected.pictureAsked} ms, ${injected.pictureAsked - inTheHtml.pictureAsked} ms later than from the HTML.`);
  • Line 2 holds when hero.js arrived, and when the browser asked for the picture.
  • Line 5 prints hero.js arrived at 317 ms.
  • Line 6 prints The picture was asked for at 324 ms, 164 ms later than from the HTML.

Both pictures lost about one round trip: the time to fetch the file that held their name. They appeared on the screen about 150 ms later too. The longer the round trip, the more a hidden name costs.

Two more ways to hide a file

  • Lazy loading with a script. Some pages write <img data-src="tea.jpg"> instead of src. A script swaps data-src for src when the picture scrolls near the screen. The scanner only reads src and srcset, so it skips these pictures. That's fine far down the page. For the main picture at the top, it's a delay.
  • Scripts added by scripts. A script made with document.createElement("script") isn't in the HTML either. The scanner can't see it, even when it's marked async.

web.dev's article on the preload scanner warns about all of these. It singles out lazy loading a picture that's on screen from the start.

Client-side rendering and late discovery

The last two versions are built a different way. The server sends an almost empty page with one script. The script runs in the browser, asks the server for the data, and builds the page from it. This is called client-side rendering: the page is built on the client, the browser, not on the server. Apps built with React or Vue can work this way.

Here is the tea page's script, app.js:

app.js, the script that builds the pageJavaScript
// Ask the server for this week's tea, then show it with its picture.fetch("tea.json")  .then((response) => response.json())  .then((tea) => {    document.querySelector("#tea").innerHTML = `      <img id="hero" src="${tea.picture}" alt="${tea.alt}" width="360" height="270">      <h1>${tea.name}</h1>      <p>${tea.about}</p>`;  });
  • Line 2 asks the server for tea.json, the data about this week's tea.
  • Line 3 reads the answer as JSON.
  • Lines 5 to 8 write the tea into the page. Line 6 is the <img> tag. Its src comes from the data, tea.picture.

So the picture's name is three steps away from the HTML. The HTML names app.js. Then app.js asks for tea.json. Then tea.json names mint.svg.

Real-life analogyA treasure hunt

In a treasure hunt, the first clue tells you where to find the second. The second tells you where to find the third. You can't skip ahead, because you don't know where the next clue is until you've read the one before. The prize is always found last.

In real life: The first clue
In JavaScript: The HTML, which names app.js
In real life: The second clue
In JavaScript: app.js, which asks for tea.json
In real life: The third clue
In JavaScript: tea.json, which names mint.svg
In real life: The prize, found last
In JavaScript: The picture

Here is the hunt, recorded:

Each file waited for the one beforePop out in the code editor (opens in a new tab)JavaScript
// The page rendered on the client. Each file waits for the one before.const askedAt = { "app.js": 161, "tea.json": 321, "mint.svg": 475 }; for (const [file, time] of Object.entries(askedAt)) {  console.log(`The browser asked for ${file} at ${time} ms.`);}
  • Line 2 holds when the browser asked for each file.
  • Lines 4 to 6 print one line for each file.
  • They print The browser asked for app.js at 161 ms. and The browser asked for tea.json at 321 ms.
  • Last comes The browser asked for mint.svg at 475 ms.

Each request started only when the one before it had arrived. Each step cost about one round trip, 150 ms. The picture was asked for at 475 ms and appeared at 692 ms. From the HTML, it appeared at 392 ms.

A preload names the picture early

Can the page tell the browser about the picture sooner? Yes. A preload is a tag in the HTML that says: "fetch this file now, the page will need it soon". The preload scanner reads it like any other tag. The last version adds one line to the head:

The preloaded page's head, shortenedHTML
<link rel="stylesheet" href="styles.css"><link rel="preload" href="mint.svg" as="image"><script src="app.js" defer></script>
  • Line 1 names the stylesheet, as before.
  • Line 2 is the preload. rel="preload" asks for an early fetch. href names the file. as="image" says what kind of file it is, so the browser fetches it the right way.
  • Line 3 names app.js, as before.

It's like writing the saffron into the recipe itself, so the helper sees it. Here is what the preload changed:

The picture came early, its tag didn'tPop out in the code editor (opens in a new tab)JavaScript
// The same page, with a preload for the picture in its head.const preloaded = { pictureAsked: 164, dataArrived: 482, picturePainted: 492 }; console.log(`The picture was asked for at ${preloaded.pictureAsked} ms, right after the HTML.`);console.log(`Its <img> tag was only made once tea.json arrived, at ${preloaded.dataArrived} ms.`);console.log(`The picture appeared on the screen at ${preloaded.picturePainted} ms.`);
  • Line 2 holds three recorded times.
  • Line 4 prints The picture was asked for at 164 ms, right after the HTML.
  • Line 5 prints Its <img> tag was only made once tea.json arrived, at 482 ms.
  • Line 6 prints The picture appeared on the screen at 492 ms.
Two rows of requests in a chain. Client-rendered: the HTML, then app.js, then tea.json, then mint.svg, each waiting for the one before; LCP 692 ms. With a preload, mint.svg starts with app.js, right after the HTML; LCP 492 ms.client-renderedHTMLapp.jstea.jsonmint.svgLCP 692 mswith a preloadHTMLapp.jstea.jsonLCP 492 msmint.svg
Each box waits for the one before it. A preload takes the picture out of the chain. But the <img> tag still waits for the script and the data.

The preload saved 200 ms: the picture appeared at 492 ms instead of 692 ms. But the picture in the HTML still won, at 392 ms. The preload fixed when the picture was found. It couldn't fix when the page was built: the <img> tag still waited for the script and the data. HTML sent from the server, with the picture already in it, fixes both.

web.dev adds a warning. Preloads compete with the page's other downloads. Use them for the few files the scanner can't find, and test each one.

Checking discovery in a waterfall

How do you check your own page? Use a waterfall: a chart with one bar per file. Each bar starts when the browser asked for the file, and ends when its last byte arrived. For each important file, ask two questions:

  1. When did its bar start: right after the HTML, or later?
  2. What asked for it?

The second answer comes from Resource Timing, the browser's record of every file a page loaded. Its initiatorType names what asked for each file. Read each row of this table as: when a file shows this initiatorType, this is what asked for it.

Who asked for each file
When initiatorType saysWhat asked for the file
linkA <link> tag asked for it: a stylesheet or a preload.
scriptA <script src> tag asked for it.
imgAn <img> tag asked for it, whether it was in the HTML or added later.
cssA stylesheet asked for it, with url(), such as a background picture.
fetchA script asked for it with fetch().

The playground below holds all six versions. Pick one, and watch where the mint.svg bar starts. Then read the word after it.

Playground: six ways to find a picture
The files0 to 1.3 s
In the HTMLrecorded
Picture asked for160 ms
Asked for byimg
Picture arrived356 ms
Picture on screen (LCP)392 ms
Try it yourself
When the picture was found

In the HTML. The browser asked for the picture at 160 ms, and the picture appeared at 392 ms. The <img> tag is in the HTML. So the browser asked for the picture as soon as the HTML arrived, together with the stylesheet.

Real loads of the same tea page in Chrome, with a 150 ms round trip added to every request, at 20 Mbps. Each bar is one file, from when the browser asked for it to its last byte. The word after the bar is what asked for the file, its initiatorType.

A late bar marked css or fetch is a file the scanner couldn't see. A late bar marked img needs a closer look. In the version where a script added the picture, the bar starts late and still says img, because a script made the tag.

Now look at this page's own files. Press Look:

Playground: this page's initiators
What Look does, shortenedJavaScript
const files = performance.getEntriesByType("resource"); const byInitiator = Object.groupBy(files, (file) => file.initiatorType);for (const [initiator, list] of Object.entries(byInitiator)) {  const first = Math.min(...list.map((file) => file.startTime));  console.log(initiator, list.length, "files, the first requested at", Math.round(first), "ms");}
This pagenot read yet

Nothing read yet.

Try it yourself
Your page

Press Look. The panel groups this page's files by what asked for them, Resource Timing's initiatorType. It shows when each group's first file was asked for.

Read from your own browser with Resource Timing. initiatorType is what asked for each file. startTime is when the browser asked for it.

In the panel's code, line 1 gets every file the page loaded. Line 3 groups the files by initiatorType. Line 5 finds the earliest request in each group.

Found early, or found late?

For each tag, ask one question: is the file's name written in the HTML? Then sort it:

Found early, or found late?
  • <img src="hero.jpg"> in the HTML
  • <script src="app.js"> in the head
  • <script src="chat.js" async> in the HTML
  • <link rel="preload" href="hero.jpg" as="image">
  • A main picture set with background-image in the CSS
  • <img data-src="hero.jpg"> swapped in by a lazy-loading script
  • A picture named in JSON that the app's script draws
  • A script added with document.createElement("script")
Try it yourself
0 of 8 correct

Sort each file by whether the preload scanner can find it in the HTML.

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

Common mix-ups

  • "The preload scanner makes blocking scripts harmless." No. It fetches the other files early, but the page still waits for the script. The recorded one held the picture back by 800 ms.
  • "The scanner reads the CSS too." No. It only reads HTML. A background picture waits for its stylesheet.
  • "async scripts are hidden from the scanner." No. An async script written in the HTML is found early. async changes when it runs. Scripts added by other scripts are the hidden ones.
  • "A preload makes client-side rendering as fast as HTML." No. It fixes when the picture is found, not when the page is built. In the recordings, the picture appeared at 492 ms with the preload, and at 392 ms from the HTML.
  • "Preload everything." No. Preloads compete with the page's other downloads. Use them for the few files the scanner can't find, and test each one.
  • "Lazy-load every picture." No. Lazy loading with a script hides the picture from the scanner. A picture on screen from the start, often the LCP, should use a plain src.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upA slower third party

The third party gets a second slower. What does the second line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// The tea page with widget.js in its head, and the same page without it.
const withTheScript = { scriptArrived: 2164, picturePainted: 2192 };
const withoutIt = { picturePainted: 392 };

console.log(`The parser waited for widget.js until ${withTheScript.scriptArrived} ms.`);
console.log(`The picture appeared ${withTheScript.picturePainted - withoutIt.picturePainted} ms later than without the script.`);

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

    Exercise 2 · Warm-upA slower stylesheet

    The stylesheet arrives 100 ms later, so the browser asks for the background picture at 418 ms. What does the second line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // The picture as a CSS background. Its name is only in styles.css.
    const background = { stylesArrived: 417, pictureAsked: 418 };
    const inTheHtml = { pictureAsked: 160 };
    
    console.log(`styles.css arrived at ${background.stylesArrived} ms.`);
    console.log(`The picture was asked for at ${background.pictureAsked} ms, ${background.pictureAsked - inTheHtml.pictureAsked} ms later than from the HTML.`);

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

      Exercise 3 · PracticeA lazy-loaded main picture

      A page's main picture is written as <img data-src="hero.jpg">, and a lazy-loading script sets src. Which finds the picture: the preload scanner or the script?

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

        Exercise 4 · PracticeThe preloaded picture's wait

        In the recorded page that was rendered on the client with a preload, the picture arrived before it could be shown. How many milliseconds passed between the picture arriving and the picture appearing on the screen?

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

          Exercise 5 · ChallengeInitiators in DevTools

          Which column of DevTools' Network panel shows what asked for each file?

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

            Exercise 6 · ChallengeFind a background picture early

            A shop's main picture is a CSS background: .hero { background: url(hero.jpg) }. What one tag in the head lets the preload scanner find hero.jpg with the HTML?

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

              Check your understanding

              For each question, ask where the file's name is written: in the HTML, in the CSS, or in a script.

              The preload scanner quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does the second line of the CSS background example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The picture as a CSS background. Its name is only in styles.css.
                const background = { stylesArrived: 317, pictureAsked: 318 };
                const inTheHtml = { pictureAsked: 160 };
                
                console.log(`styles.css arrived at ${background.stylesArrived} ms.`);
                console.log(`The picture was asked for at ${background.pictureAsked} ms, ${background.pictureAsked - inTheHtml.pictureAsked} ms later than from the HTML.`);

                Choose an answer to see the explanation.

              2. Question 2 of 8What does the preload scanner read?

                Choose an answer to see the explanation.

              3. Question 3 of 8Why does a plain <script src> in the head stop the parser?

                Choose an answer to see the explanation.

              4. Question 4 of 8In the recorded page behind a blocking script, why had the picture arrived long before the parser reached its tag?

                Choose an answer to see the explanation.

              5. Question 5 of 8Why can't the preload scanner find a picture that a script adds to the page?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does the third line of the client-rendered chain example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The page rendered on the client. Each file waits for the one before.
                const askedAt = { "app.js": 161, "tea.json": 321, "mint.svg": 475 };
                
                for (const [file, time] of Object.entries(askedAt)) {
                  console.log(`The browser asked for ${file} at ${time} ms.`);
                }

                Choose an answer to see the explanation.

              7. Question 7 of 8In the recordings, which put the picture on the screen sooner: the client-rendered page with a preload, or the picture written in the HTML?

                Choose an answer to see the explanation.

              8. Question 8 of 8What initiatorType does Resource Timing give a picture set with background-image in a stylesheet?

                Choose an answer to see the explanation.

              Key takeaways

              • The parser reads the HTML a piece at a time, as it arrives, and builds the page.
              • A script tag without async or defer stops the parser until the script has downloaded and run.
              • The preload scanner skims ahead in the same HTML and fetches the files it names, even while the parser waits.
              • It only reads HTML. A file named in CSS, added by a script, or named in data is found late.
              • A preload fixes when a file is found. HTML from the server, with the file in it, also fixes when it's shown.
              • In a waterfall, check when each important file's bar starts, and what asked for it.

              Remember the one-liner.
              If its name isn't in the HTML, the browser finds it late.

              Sources

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

              Coming next: Render-blocking and parser-blocking resources. Here, one script stopped the parser. Next: why CSS holds back the first paint, and what async, defer and type="module" change.

              CompleteFrontend Clear concepts. Working examples.