completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

Resource hints

dns-prefetch, preconnect, preload, modulepreload, and prefetch: what each does and when it backfires. Measured in Chrome over HTTP/2 on a slow link: preconnect with and without crossorigin and in an incognito window, a font preload's trade-off, a module chain with and without modulepreload, two needless preloads against the LCP image, and the next page's script prefetched.

By the end, you can
  • 01
    Pick the hint for what you knowA server you'll need: preconnect. A file this page needs: preload, or modulepreload for a module. A file for the next page: prefetch.
  • 02
    Get the details rightAdd crossorigin where the file comes without cookies, as fonts do. Give a preload its as and type. Test in a normal window.
  • 03
    Spot hints that backfireFind preloads that push the main image back, and the ones Chrome's console says were never used.

Telling the browser early

Asha opens the tea shop's spring sale on her phone. In about a second, a line of text appears: "Everything in the shop is 20% off until Sunday." But the big heading above it, Spring sale, stays blank. It shows up almost a second later.

The heading is drawn in a handwritten font. The letters of that font live in a file on another server. Why did the browser fetch that file so late? And could the page have told it sooner? This lesson answers both, with five kinds of hint, and measures what each one cost.

How the course timed it

The course built the shop and loaded its pages in Chrome:

  • The shop lives at shop.test. Its sale page holds a heading and one line of text.
  • The heading's font comes from a font service, in two parts. A stylesheet on fonts.test says which font file to use. The file itself, caveat.woff2, lives on files.fonts.test.
  • Each of the three is its own server, speaking HTTP/2 over HTTPS.
  • A round trip, a message there and its answer back, took 150 ms. Every download shared one slow link of 1.6 Mbps.
  • Server names were looked up instantly, so no visit waited for an address.
  • Each run was a new Chrome with a new profile, like a first-time visitor. Each version of a page was run 5 times. The lesson quotes the middle value, the median.

Here is what Asha saw, with no hints:

The text first, the heading much laterPop out in the code editor (opens in a new tab)JavaScript
// Asha's visit to the sale page with no hints, in ms: the median of 5 runs.const visit = { textPainted: 992, headingPainted: 1832 }; console.log(`The text appeared at ${visit.textPainted} ms.`);console.log(`The heading appeared at ${visit.headingPainted} ms.`);console.log(`The heading waited ${visit.headingPainted - visit.textPainted} ms longer.`);
  • Line 1 says the times are in milliseconds (ms), counted from the start of the visit.
  • Line 2 holds two times. textPainted is when the line of text appeared. It was the first thing on the screen, so it's the page's First Contentful Paint (FCP).
  • headingPainted is when the heading appeared. It was the largest thing on the screen, so it's the page's Largest Contentful Paint (LCP).
  • Lines 4 and 5 print The text appeared at 992 ms. and The heading appeared at 1832 ms.
  • Line 6 prints The heading waited 840 ms longer.

Why would the heading wait? The font service's stylesheet says font-display: block. That asks the browser to draw the heading invisibly for a short while, until its font arrives. So the heading appears when the font does. And the font came late. Follow the browser, step by step:

  1. It asks shop.test for the page. The page names the stylesheet on fonts.test.
  2. It opens a connection to fonts.test and gets the stylesheet. Inside is the font's address, on files.fonts.test.
  3. Only now does it open a connection to files.fonts.test and ask for the font.

It's the note inside the jar from HTML parsing and the preload scanner: a file named inside another file is found only once that file has arrived. Here is the whole chain in one picture:

The sale page's chain. The page, on shop.test, is asked for first. It names a stylesheet on fonts.test. The stylesheet names the font file, caveat.woff2, on files.fonts.test. Each needs a new connection. A curved arrow from the page to the font file is a hint in the page's head, which reaches it at once.asked for firstsale pageshop.testnew connectionnamed in the pagestylesheetfonts.testnew connectionnamed in the stylesheetcaveat.woff2files.fonts.testnew connectiona hint in the head reaches it at once
Without hints, the browser finds each server only when the file before it arrives, and opens a new connection each time. A hint in the page's head names the last one at the start.

You knew all of this when you wrote the page. You knew the font's server, and even the font file's name. The browser had to find them one step at a time. A resource hint lets you tell it at the start. It's a <link> tag in the page's head, and its rel attribute says which hint it is. There are five:

  • dns-prefetch: "You'll need this server later. Look up its address now."
  • preconnect: "You'll need this server soon. Connect to it now."
  • preload: "This page needs this file. Fetch it now."
  • modulepreload: "This page needs this JavaScript module. Fetch it and get it ready."
  • prefetch: "The next page will probably need this file. Fetch it when you have time."
Real-life analogyA line at the top of the recipe

In the preload scanner lesson, a helper skimmed the recipe and fetched every ingredient it named. But the spice jar held a note: "This mix needs saffron." So someone made a second trip to the shop. Next time, the cook writes one line at the top of the recipe: "You'll also need saffron." The helper brings it back on the first trip.

In real life: The recipe
In a page load: The page's HTML
In real life: The helper, skimming it for ingredients
In a page load: The browser, reading ahead for files
In real life: The note inside the spice jar
In a page load: The font's address, inside the stylesheet
In real life: "You'll also need saffron", written at the top
In a page load: A resource hint in the page's head
The main idea

A resource hint is a line in the page's head. It tells the browser about work it will need soon: a server to get ready, a file this page needs, or a file for the next page. Hint early, hint exactly, and hint only what matters. Every hint costs something, as the end of this lesson shows.

The course recorded twelve versions of the shop's pages: with no hints, with each hint, and with each mistake in this lesson. Each button below is one version. Pick Font, no hints to see Asha's visit. Then come back to the playground as each hint is explained.

Playground: five hints, measured
What happened
  1. FCP992 ms
  2. LCP1,516 ms
  3. Hints in the page<link rel="preconnect" href="https://fonts.test"> <link rel="preconnect" href="https://files.fonts.test" crossorigin>
  4. Chrome's console warningsnone
  5. Connection 1, to shop.test/font (u=0, i)
  6. Connection 2, to shop.testopened, never used
  7. Connection 3, to fonts.test/css?family=Caveat (u=0)
  8. Connection 4, to files.fonts.test/caveat.woff2 (u=0)
Try it yourself
preconnect, with crossorigin: LCP 1,516 ms

FCP and LCP are the middle value of 5 runs, each in a new Chrome. The warnings, connections and requests come from the run whose LCP was that middle value. Each request shows its Priority header in brackets. A connection that carried no request was opened for nothing. In a normal window, Chrome also opened a spare connection to shop.test as the visit started.

Recorded in Chrome 156 on Apple M1 Pro, over HTTP/2, with a 150 ms round trip and 1.6 Mbps down. Server names were looked up instantly.

Two other lessons use these hints. Loading strategies, in the system design course, decides what a page loads first and what it leaves for later. And React can write the tags for you, with its preload and preconnect functions, as Metadata, stylesheets and resource hints shows.

preconnect and dns-prefetch

Look at step 3 again. Before the browser could ask for the font, it needed a connection to files.fonts.test. web.dev lists what a secure connection takes:

  1. Look up the server's address. This is the DNS lookup.
  2. Set up a connection to it. This is TCP.
  3. Make the connection private. This is TLS, the S in HTTPS.

In each step, the browser sends a message and waits for the answer: a round trip. Here the addresses came instantly. But the TCP and TLS steps still cost at least one round trip each, 150 ms apiece. TCP connections and slow start and TLS without the wait took them apart. And the browser couldn't start them until the stylesheet had arrived.

Real-life analogyPhoning the restaurant on your way

You're driving to a small restaurant on a quiet night. On the way, you phone: "We'll be there in twenty minutes." They unlock the door, switch on the lights and warm the oven. You haven't ordered anything yet. But when you walk in, you can order at once.

In real life: The restaurant
In a page load: The font's server, files.fonts.test
In real life: Your call from the car
In a page load: <link rel="preconnect"> in the page's head
In real life: Unlocking the door and warming the oven
In a page load: The DNS lookup, TCP and TLS, done early
In real life: Ordering the moment you walk in
In a page load: The font's request, sent on a connection that's ready

preconnect is that call. The course added two to the sale page's head:

The sale page's two preconnectsHTML
<link rel="preconnect" href="https://fonts.test"><link rel="preconnect" href="https://files.fonts.test" crossorigin>
  • Line 1 is for the stylesheet's server, fonts.test.
  • Line 2 is for the font file's server, files.fonts.test. It ends with crossorigin, which comes up in a moment.
The heading, with and without preconnectPop out in the code editor (opens in a new tab)JavaScript
// When the heading appeared, in ms: the median of 5 runs.const headingPainted = {  "no hints": 1832,  "preconnect": 1516,}; const saved = headingPainted["no hints"] - headingPainted["preconnect"];console.log(`With preconnect, the heading appeared ${saved} ms sooner.`);
  • Lines 2 to 5 hold when the heading appeared: at 1832 ms with no hints, and at 1516 ms with the preconnects.
  • Line 7 takes one from the other.
  • Line 8 prints With preconnect, the heading appeared 316 ms sooner.

That's more than two round trips. Chrome did the TCP and TLS steps while the stylesheet was still on its way. So when the stylesheet named the font, only the request was left. Why it matters: a preconnect saves whole round trips, so it saves the most where round trips are long.

dns-prefetch: only the address

Sometimes a whole connection is more than you need. dns-prefetch does only the first step: it looks up the server's address. It's like looking up a shop's address before you leave home, without walking there yet. MDN calls it identical to preconnect "except that it only handles the DNS part."

So it costs less, and saves less. web.dev suggests preconnect for "only the most critical connections", and dns-prefetch for the rest. Say the sale page also loaded reviews from reviews.test, after its first paint:

A whole connection, and only an addressHTML
<link rel="preconnect" href="https://files.fonts.test" crossorigin>
<link rel="dns-prefetch" href="https://reviews.test">
  • Line 1 gets the font's server fully ready. The heading needs it for the first view.
  • Line 2 only looks up reviews.test. The reviews come later, so the address is enough for now.

The course's server names were looked up instantly, so it couldn't measure dns-prefetch here. DNS lookups did. Neither hint helps with your own server: the browser already has a connection to it, the one that brought the page. Both are for other servers.

Why the font's preconnect needs crossorigin

Line 2 of the preconnects ended with crossorigin, and line 1 didn't. Why? It has to do with credentials: the things a browser can send to tell a server who you are, like cookies. Some requests carry them, and some don't:

  • The stylesheet on fonts.test is fetched with credentials, like any plain stylesheet.
  • A font from another server is fetched without them. web.dev says fonts are "loaded in anonymous mode".

The Fetch Standard keeps the two apart. It files every connection under two things. One is its origin: the scheme, server name and port, like https://files.fonts.test. The other is whether it carries credentials. So a request without credentials can't use a connection opened with them. Connections in practice called these a members' door and a guests' door: you can only share a trip if you use the same door.

A preconnect opens the members' door, with credentials, unless you add crossorigin. The HTML standard spells it out: with crossorigin, a preconnect to another origin goes without credentials. A font from another server always comes in through the guests' door. So the font's preconnect needs crossorigin, and the stylesheet's doesn't.

The course tried the preconnects without crossorigin on line 2:

A preconnect the font couldn't usePop out in the code editor (opens in a new tab)JavaScript
// Both preconnects, but no crossorigin on the second. Connections were// the same in every run; the time is the median of 5 runs, in ms.const noCrossorigin = { connectionsToFontServer: 2, headingPainted: 1832 }; console.log(`Chrome opened ${noCrossorigin.connectionsToFontServer} connections to files.fonts.test.`);console.log(`The heading appeared at ${noCrossorigin.headingPainted} ms, as with no hints.`);
  • Lines 1 and 2 say what was measured.
  • Line 3 holds the results: 2 connections to the font's server, and the heading at 1832 ms.
  • Line 5 prints Chrome opened 2 connections to files.fonts.test.
  • Line 6 prints The heading appeared at 1832 ms, as with no hints.

Chrome opened the preconnect's connection, with credentials. The font couldn't use it, so the font opened a second one, as late as before. web.dev says that without crossorigin, "the browser only performs the DNS lookup." Chrome did more than that here, but it saved nothing. Why it matters: a preconnect without crossorigin looks right in the HTML and does nothing for a font.

Not in an incognito window

One more surprise. In an incognito window, the same two preconnects did nothing at all. The heading appeared as late as with no hints. Chrome carries out these hints through a part of itself called the loading predictor. Its source code says of that predictor: "Disabled for off-the-record. Policy choice, not a technical limitation." Off-the-record means incognito. So test your hints in a normal window.

preload and modulepreload

With the preconnect, the font's server was ready early. But the browser still asked for the font only once the stylesheet had named it. You know the font's exact address. Why not ask for it at once?

That's what preload does. The HTML standard says the browser "must" fetch a preloaded file, for the current page. web.dev: "Preloading is best suited for resources typically discovered late by the browser." The font is one of those.

Real-life analogyAsking the kitchen to start your main course

On the phone, you go one step further: "Please start the lamb curry now. It takes the longest." When you sit down, it's nearly ready. But the kitchen has only so many hands. While they work on your curry, the bread for your table comes a little later.

In real life: Naming the exact dish
In a page load: href, as and type: the exact file, and what kind it is
In real life: The kitchen starting it before you arrive
In a page load: The browser fetching the font before the stylesheet names it
In real life: The curry, nearly ready when you sit down
In a page load: The heading, painted sooner
In real life: The bread coming a little later
In a page load: The stylesheet, and the first paint, coming later

Here is the font's preload, all on one line:

Preloading the fontHTML
<link rel="preload" href="https://files.fonts.test/caveat.woff2" as="font" type="font/woff2" crossorigin>

Read it one attribute at a time:

  • rel="preload" makes it a preload.
  • href is the exact file. A preconnect needs only the server, but a preload needs the file's full address.
  • as="font" says what kind of file it is. web.dev says this helps the browser set the file's priority, send the right headers, and find it in the cache later.
  • type="font/woff2" names the file's format. A browser that can't read that format skips the preload.
  • crossorigin fetches it without credentials, the same way the font itself is fetched.
The heading sooner, the text laterPop out in the code editor (opens in a new tab)JavaScript
// The sale page in ms, the median of 5 runs: with no hints, and with the font preloaded.const noHints = { textPainted: 992, headingPainted: 1832 };const preloaded = { textPainted: 1360, headingPainted: 1360 }; console.log(`The heading appeared ${noHints.headingPainted - preloaded.headingPainted} ms sooner.`);console.log(`The text appeared ${preloaded.textPainted - noHints.textPainted} ms later.`);
  • Line 2 holds the two paints with no hints. Line 3 holds them with the font preloaded.
  • Line 5 prints The heading appeared 472 ms sooner.
  • Line 6 prints The text appeared 368 ms later.

The heading appeared even sooner than with the preconnect, as hoped. But the text, the page's first paint, now came later. Why would fetching one file early hold back the rest of the page?

The stylesheet came later tooPop out in the code editor (opens in a new tab)JavaScript
// When the font service's stylesheet finished arriving, in ms: the median of 5 runs.const stylesheetArrived = { noHints: 962.6, preloaded: 1342.3 }; console.log(`With no hints, the stylesheet arrived at ${stylesheetArrived.noHints} ms.`);console.log(`With the font preloaded, it arrived at ${stylesheetArrived.preloaded} ms.`);
  • Line 2 holds when the font service's stylesheet finished arriving, in both versions.
  • Lines 4 and 5 print With no hints, the stylesheet arrived at 962.6 ms. and With the font preloaded, it arrived at 1342.3 ms.

A stylesheet in the head is render-blocking: the browser paints nothing until it has arrived, as Render-blocking and parser-blocking resources showed. So the text waited for the stylesheet. And the stylesheet waited for the font.

Both files were asked for at the same moment, over one slow link. The font file is about 75 kB. The stylesheet is one short line. In every run, the font's bytes finished arriving first, and the stylesheet's just after. That's the bread coming late.

On this page, the heading mattered more than the text, so the preload paid off. Why it matters: a preload takes the link's time from something else. After you add one, check FCP as well as LCP. Here is every version of the sale page so far:

When the sale page's heading appeared: the median LCP of 5 runs. Font, no hints: 1,832 ms. preconnect, with crossorigin: 1,516 ms. The same, in an incognito window: 1,832 ms. preconnect, without crossorigin: 1,832 ms. preload the font, with crossorigin: 1,360 ms.Font, no hints1,832 mspreconnect, with crossorigin1,516 msThe same, in an incognito window1,832 mspreconnect, without crossorigin1,832 mspreload the font, with crossorigin1,360 ms
Each bar ends when the heading appeared, the median of 5 runs. Shaded: the hint made it appear sooner.

A preload without crossorigin

web.dev warns: "Fonts preloaded without the crossorigin attribute will be fetched twice!" The preload would fetch the font with credentials, but the font's own request goes without them. The course tried it, and Chrome's console said this, word for word:

Chrome's console, after a preload without crossoriginText
A preload for 'https://files.fonts.test/caveat.woff2' is found, but is not used because the request credentials mode does not match. Consider taking a look at crossorigin attribute.The resource https://files.fonts.test/caveat.woff2 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally.
  • Line 1 says the preload was found but not used, because "the request credentials mode does not match". The credentials mode is whether a request carries credentials.
  • Line 2 came a few seconds after the page loaded. It says the preloaded file was never used.

On this shop, the font still crossed the link only once. The font server let any site use the file and let it be cached, so the font's own request found it in the HTTP cache. Don't count on that: add crossorigin.

modulepreload: a chain of modules

JavaScript modules can form a chain of their own. A module is a JavaScript file that can import other files. web.dev: "the browser needs to wait for a module to load before it finds out what its dependencies are." A module's dependencies are the files it imports.

The shop's modules page has three. app.mjs draws the page. It imports cart.mjs, which imports format.mjs. Here is when Chrome asked for each one, with no hints:

One module at a timePop out in the code editor (opens in a new tab)JavaScript
// When Chrome asked for each module with no hints, in ms: the median of 5 runs.const asked = { "app.mjs": 482.7, "cart.mjs": 644.2, "format.mjs": 800.1 }; for (const [file, at] of Object.entries(asked)) console.log(`${file} was asked for at ${at} ms.`);
  • Line 2 holds when Chrome asked for each module.
  • Line 4 prints one line for each: app.mjs was asked for at 482.7 ms., cart.mjs was asked for at 644.2 ms. and format.mjs was asked for at 800.1 ms.

Each module was asked for about one round trip after the one before. It's the treasure hunt from the preload scanner lesson: each clue tells you where the next one is. modulepreload hands over every clue at the start:

The modules page's hintsHTML
<link rel="modulepreload" href="/cart.mjs"><link rel="modulepreload" href="/format.mjs"><script type="module" src="/app.mjs"></script>
  • Lines 1 and 2 name the two imports, cart.mjs and format.mjs.
  • Line 3 is the page's own script tag. The browser finds app.mjs there anyway, so it needs no hint.
The modules page, with and without the hintsPop out in the code editor (opens in a new tab)JavaScript
// When the modules page painted, in ms: the median of 5 runs.const painted = { noHints: 980, modulepreload: 668 }; console.log(`With no hints, the page painted at ${painted.noHints} ms.`);console.log(`With modulepreload, it painted at ${painted.modulepreload} ms.`);
  • Line 2 holds when the page painted in each version. The modules draw the page, so it paints only after all three run.
  • Lines 4 and 5 print With no hints, the page painted at 980 ms. and With modulepreload, it painted at 668 ms.

With the hints, Chrome asked for all three modules together, at about 484 ms. Two round trips of waiting were gone. Why it matters: every level of imports costs a round trip, and web.dev suggests a flat list of preloads for deep chains.

Why not a plain preload for a module? Two reasons:

  • It can fetch the module twice. MDN says modulepreload knows the file is a module, so it "can use the correct credentials modes to avoid double-fetching."
  • It stops at the download. With modulepreload, MDN says the browser "downloads it, then parses and compiles it directly into the in-memory module map." Parsing and compiling turn the text into code that can run. The module map is the page's list of modules it has loaded.

The HTML standard lets a browser fetch a preloaded module's own imports too, but it doesn't have to. So web.dev advises listing every module in the chain, as lines 1 and 2 do.

prefetch

So far, every hint helped the page Asha was on. prefetch helps the page she'll open next.

The shop has a list of teas. Say most visitors click Moroccan mint tea. Its product page is drawn by React, in a 219 kB script called product.js. The page shows nothing until that script has arrived and run.

Real-life analogyTomorrow's lunch in the fridge

On Sunday evening, after dinner, you make Monday's lunch and put it in the fridge. The kitchen is quiet, so it gets in nobody's way. On Monday at noon, lunch takes one minute. If you end up eating out, you made it for nothing.

In real life: Making it after dinner, when the kitchen is quiet
In a page load: Fetching at a low priority, after what this page needs
In real life: The fridge
In a page load: The HTTP cache
In real life: Lunch the next day, ready in a minute
In a page load: The next page, with its script already here
In real life: Eating out instead
In a page load: A visitor who goes elsewhere: the bytes were spent for nothing

The course put one hint in the list page's head:

The list page's prefetchHTML
<link rel="prefetch" href="/product.js">

It names the product page's script. The HTML standard says the browser "should" fetch it, "as it is likely to be required for a followup navigation." A navigation is going to another page. web.dev adds that prefetching "takes place at the 'Lowest' priority", so it doesn't compete with the page you're on. Then Asha read the list and opened the product page:

The product page, with and without the prefetchPop out in the code editor (opens in a new tab)JavaScript
// The product page, opened from the list. Times are the median of 5 runs, in ms.// Bytes are what product.js took on the wire, the same in every run.const noPrefetch = { painted: 1756, scriptBytes: 219377 };const prefetched = { painted: 500, scriptBytes: 0 }; console.log(`No prefetch: the script took ${noPrefetch.scriptBytes} bytes; the page painted at ${noPrefetch.painted} ms.`);console.log(`Prefetched: the script took ${prefetched.scriptBytes} bytes; the page painted at ${prefetched.painted} ms.`);
  • Lines 3 and 4 hold what was measured on the product page: when it painted, and how many bytes the script took on the wire.
  • Line 6 prints No prefetch: the script took 219377 bytes; the page painted at 1756 ms.
  • Line 7 prints Prefetched: the script took 0 bytes; the page painted at 500 ms.

With the prefetch, the script was already in the HTTP cache. Nothing crossed the link, and the page painted 1,256 ms sooner.

When the product page painted, after the list: the median LCP of 5 runs. Product page, no prefetch: 1,756 ms. Product page, prefetched: 500 ms.Product page, no prefetch1,756 msProduct page, prefetched500 ms
Each bar ends when the product page painted, opened from the list. Shaded: its script was prefetched while Asha read the list.

Did the prefetch cost the list page anything? Chrome tells the server how urgent each request is, in a Priority header. Its urgency runs from 0, the most urgent, to 7. Chrome sent the prefetch with a lower urgency than the list page itself. It finished arriving at 1,746.5 ms, long after the list had painted. Exercise 2 checks the list page's paint, and exercise 4 asks for the exact header.

Two rules make a prefetch pay off:

  • Prefetch only what visitors will probably need. web.dev: prefetch "when you are confident that users will need them." Every prefetch costs bytes, even for visitors who never click.
  • Prefetch only files that can be cached. web.dev: a prefetched file is kept in the HTTP cache "if the resource is cacheable, otherwise it will be discarded and not be used."

Hints in Link headers

Every hint so far was a tag in the HTML. A server can send the same hints as a response header, called Link:

Two hints as response headersText
Link: <https://fonts.test>; rel=preconnect
Link: </hero.jpg>; rel=preload; as=image
  • Line 1 is a preconnect to fonts.test. The address goes between < and >, and rel says which hint it is.
  • Line 2 preloads a picture, hero.jpg. The tag's as attribute becomes one more part, after a semicolon.

Why send a header instead of a tag? The browser reads a response's headers before its body. web.dev: a hint in the header "doesn't rely on markup being parsed, and it can be triggered by requests for stylesheets, scripts, and more." So a font service can send a preconnect with its stylesheet, naming the server its font files live on. web.dev says Google Fonts does just that.

It's like saying the hint out loud at the door instead of writing it on the menu. And a server can send these headers even sooner, in a 103 response, while it's still building the page. That's the waiter who tells the bar early, from 103 Early Hints.

Over-preloading and unused preloads

Every hint asks the browser to spend something early: a connection, or time on the link. Ask for too much, and the things that matter wait.

Real-life analogyToo many "please get ready" calls

On the way to the restaurant, you phone five times. Start the curry. Warm the bread. Chill the juice. Bake a cake. Set a table in the garden. The kitchen does it all, because you asked. Now the curry, the dish you came for, waits behind the cake. And half of what they made, nobody eats.

In real life: Each "please get ready" call
In a page load: Each preload or preconnect
In real life: The kitchen doing everything you asked
In a page load: The browser fetching every preload
In real life: The curry waiting behind the cake
In a page load: The gallery's big picture waiting behind the fonts
In real life: Food nobody eats
In a page load: Preloaded files the page never uses

A preload is the call the kitchen can't ignore. The HTML standard says the browser "must" fetch it, and only "should" act on the other hints. web.dev says preload "is mandatory for the browser", so use it "sparingly and only preload the most critical resources."

The course tried it on the shop's gallery. Its big picture at the top, the hero, is marked fetchpriority="high". That's the regular who says "this one's urgent", from How browsers prioritize requests. Then two preloads were added, for fonts the page never uses:

Two needless preloadsPop out in the code editor (opens in a new tab)JavaScript
// The gallery's LCP in ms, the median of 5 runs, and Chrome's console warnings, the same in every run.const noPreloads = { lcp: 1176, warnings: 0 };const twoUnusedFonts = { lcp: 1672, warnings: 2 }; console.log(`With no preloads, the hero painted at ${noPreloads.lcp} ms, with ${noPreloads.warnings} warnings.`);console.log(`With two unused fonts preloaded, it painted at ${twoUnusedFonts.lcp} ms, with ${twoUnusedFonts.warnings} warnings.`);
  • Line 2 holds the gallery with no preloads. Line 3 holds it with the two unused fonts preloaded.
  • Line 5 prints With no preloads, the hero painted at 1176 ms, with 0 warnings.
  • Line 6 prints With two unused fonts preloaded, it painted at 1672 ms, with 2 warnings.

The hero came 496 ms later, for two files nobody used. Why? Look at how urgent Chrome said each request was:

As urgent as the heroPop out in the code editor (opens in a new tab)JavaScript
// The Priority header Chrome sent with three of the gallery's requests, the same in every run.const priority = {  "mono.woff2": "u=1",  "mono-italic.woff2": "u=1",  "hero.jpg": "u=1, i",}; for (const [file, header] of Object.entries(priority)) console.log(`${file} was asked for with ${header}.`);
  • Lines 3 to 5 hold the Priority header of the two fonts and the hero.
  • Line 8 prints mono.woff2 was asked for with u=1., mono-italic.woff2 was asked for with u=1. and hero.jpg was asked for with u=1, i.

u=1 is an urgency of 1, on the scale from 0 to 7. RFC 9218: "The smaller the value, the higher the precedence." The i means the hero is useful in pieces, as it arrives. The course's shop server sends the most urgent files first. The fonts were as urgent as the hero, and they were asked for first. So in every run, both fonts finished arriving before the hero did.

When the gallery's hero appeared: the median LCP of 5 runs. Gallery, no preloads: 1,176 ms. Gallery, two needless preloads: 1,672 ms.Gallery, no preloads1,176 msGallery, two needless preloads1,672 ms
Each bar ends when the hero appeared, the median of 5 runs. Shaded: with two fonts preloaded that the page never uses.

Chrome noticed. Its console warned about each font, a few seconds after the load event. Each one "was preloaded using link preload but not used within a few seconds from the window's load event." So read the console after you add preloads, and remove each one it warns about.

Too many preconnects cost something too. web.dev: "Unnecessary preconnecting can delay other important resources, so limit the number of preconnected domains and test the impact preconnecting makes." Its preload article puts the rule in one line: "If too many resources are prioritized, effectively none of them are."

Which hint fits?

Each hint fits a different thing you know. Read each row across: the hint, what you need to know to use it, what the browser does early, and whether it must.

The five hints side by side
HintWhat you knowWhat the browser does earlyMust the browser do it?
dns-prefetchA server the page will use later.It looks up the server's address.No. The browser may skip it.
preconnectA server the page will use soon.It looks up the address, connects, and sets up TLS.No. The browser may skip it.
preloadA file this page needs, found late.It fetches the file now, at the priority its as gives.Yes. The browser must fetch it.
modulepreloadA module this page needs.It fetches the module and gets it ready to run.Yes. The browser must fetch it.
prefetchA file the next page will probably need.It fetches the file at the lowest priority, into the HTTP cache.No. The browser may skip it or wait.

Now sort each case by the hint that fits it.

Which hint fits?
  • The server your font files come from, when the file names change with each version
  • Another server the page calls right after it loads
  • The one font file the heading uses
  • The main image, set as a background in the CSS
  • Modules your first module imports, two levels deep
  • The module for a screen the page draws at once
  • The product page's script, from a list most visitors click through
  • The checkout's script, from the basket page
Try it yourself
0 of 8 correct

Sort each case by the hint that fits it.

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

Common mix-ups

  • "Hints work wherever Chrome runs." No. In an incognito window, Chrome ignored the preconnects. The heading appeared at 1,832 ms with or without them.
  • "crossorigin on a preconnect doesn't matter." No. Without it, the font needed a second connection, and the heading appeared no sooner.
  • "A preload only ever helps." No. The font's preload made the heading 472 ms sooner and the text 368 ms later.
  • "Extra preloads are harmless." No. Two unused fonts made the gallery's hero appear at 1,672 ms instead of 1,176.
  • "A prefetch slows the current page." No. Chrome sends it at a low urgency, after what the current page needs.
  • "A preconnect to my own site speeds it up." No. The browser already has that connection: the one that brought the page.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upIncognito

What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// When the heading appeared, in ms: the median of 5 runs.
const headingPainted = {
  "no hints": 1832,
  "preconnect, normal window": 1516,
  "preconnect, incognito window": 1832,
};

const saved = headingPainted["no hints"] - headingPainted["preconnect, incognito window"];
console.log(`In an incognito window, preconnect saved ${saved} ms.`);

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

    Exercise 2 · Warm-upThe list page

    What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // When the list page itself painted, in ms: the median of 5 runs.
    const listPainted = {
      "no prefetch": 508,
      "with the prefetch": 508,
    };
    
    const slower = listPainted["with the prefetch"] - listPainted["no prefetch"];
    console.log(`The prefetch made the list page ${slower} ms slower.`);

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

      Exercise 3 · PracticeA preload without crossorigin

      With the font preloaded but no crossorigin, when did the heading appear?

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

        Exercise 4 · PracticeThe prefetch's priority

        Which Priority header did the prefetch of product.js carry?

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

          Exercise 5 · ChallengeUnused connections

          After how many seconds does the browser close a preconnected connection that wasn't used?

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

            Exercise 6 · ChallengeUnused preloads

            About how long after the load event does Chrome warn about an unused preload?

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

              Check your understanding

              For each question, ask three things. What did the hint make the browser do early? Could the later request use it? And what did it take the link's time from?

              Resource hints quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does line 6 of the font preload example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The sale page in ms, the median of 5 runs: with no hints, and with the font preloaded.
                const noHints = { textPainted: 992, headingPainted: 1832 };
                const preloaded = { textPainted: 1360, headingPainted: 1360 };
                
                console.log(`The heading appeared ${noHints.headingPainted - preloaded.headingPainted} ms sooner.`);
                console.log(`The text appeared ${preloaded.textPainted - noHints.textPainted} ms later.`);

                Choose an answer to see the explanation.

              2. Question 2 of 8What does line 7 of the prefetch example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The product page, opened from the list. Times are the median of 5 runs, in ms.
                // Bytes are what product.js took on the wire, the same in every run.
                const noPrefetch = { painted: 1756, scriptBytes: 219377 };
                const prefetched = { painted: 500, scriptBytes: 0 };
                
                console.log(`No prefetch: the script took ${noPrefetch.scriptBytes} bytes; the page painted at ${noPrefetch.painted} ms.`);
                console.log(`Prefetched: the script took ${prefetched.scriptBytes} bytes; the page painted at ${prefetched.painted} ms.`);

                Choose an answer to see the explanation.

              3. Question 3 of 8How is dns-prefetch different from preconnect?

                Choose an answer to see the explanation.

              4. Question 4 of 8A preconnect without crossorigin didn't speed up the font. Why?

                Choose an answer to see the explanation.

              5. Question 5 of 8The same preconnect did nothing in an incognito window. Why?

                Choose an answer to see the explanation.

              6. Question 6 of 8Why did modulepreload make the modules page paint sooner?

                Choose an answer to see the explanation.

              7. Question 7 of 8Two preloaded fonts the page never used made the hero appear later. Why?

                Choose an answer to see the explanation.

              8. Question 8 of 8Did the prefetch slow down the list page that held it?

                Choose an answer to see the explanation.

              Key takeaways

              • Preconnect to a server you'll need soon, with crossorigin for fonts. On the sale page, the heading appeared at 1,516 ms instead of 1,832.
              • dns-prefetch only looks up the address. Use it for servers the page needs later.
              • Preload a late file you can name, with as, type and crossorigin. Then check FCP too: the font's preload made the text 368 ms later.
              • Modulepreload a module's imports. The modules page painted at 668 ms instead of 980.
              • Prefetch the next page's files, if they can be cached. The product page painted at 500 ms instead of 1,756.
              • Every preload competes for the link. Remove the ones Chrome warns about, and test hints outside incognito.

              Remember the one-liner.
              Hint early, hint exactly, and hint only what matters.

              Sources

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

              Coming next: How LCP works, the first lesson of Loading content fast: LCP. Which elements count, how candidates change during load, and when the browser stops looking.

              CompleteFrontend Clear concepts. Working examples.