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.
- 01Pick 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.
- 02Get 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.
- 03Spot 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.testsays which font file to use. The file itself,caveat.woff2, lives onfiles.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:
// 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.
textPaintedis when the line of text appeared. It was the first thing on the screen, so it's the page's First Contentful Paint (FCP). headingPaintedis 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.andThe 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:
- It asks
shop.testfor the page. The page names the stylesheet onfonts.test. - It opens a connection to
fonts.testand gets the stylesheet. Inside is the font's address, onfiles.fonts.test. - Only now does it open a connection to
files.fonts.testand 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:
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."
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
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.
- FCP992 ms
- LCP1,516 ms
- Hints in the page<link rel="preconnect" href="https://fonts.test"> <link rel="preconnect" href="https://files.fonts.test" crossorigin>
- Chrome's console warningsnone
- Connection 1, to shop.test/font (u=0, i)
- Connection 2, to shop.testopened, never used
- Connection 3, to fonts.test/css?family=Caveat (u=0)
- Connection 4, to files.fonts.test/caveat.woff2 (u=0)
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.
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:
- Look up the server's address. This is the DNS lookup.
- Set up a connection to it. This is TCP.
- 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.
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:
<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 withcrossorigin, which comes up in a moment.
// 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:
<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.testis 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:
// 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.
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,asandtype: 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:
<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.hrefis 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.crossoriginfetches it without credentials, the same way the font itself is fetched.
// 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?
// 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.andWith 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:
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:
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:
// 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.andformat.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:
<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.mjsandformat.mjs. - Line 3 is the page's own script tag. The browser finds
app.mjsthere anyway, so it needs no hint.
// 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.andWith 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
modulepreloadknows 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.
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:
<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, 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.
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:
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>, andrelsays which hint it is. - Line 2 preloads a picture,
hero.jpg. The tag'sasattribute 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.
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:
// 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:
// 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
Priorityheader 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.andhero.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.
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.
| Hint | What you know | What the browser does early | Must the browser do it? |
|---|---|---|---|
dns-prefetch | A server the page will use later. | It looks up the server's address. | No. The browser may skip it. |
preconnect | A server the page will use soon. | It looks up the address, connects, and sets up TLS. | No. The browser may skip it. |
preload | A file this page needs, found late. | It fetches the file now, at the priority its as gives. | Yes. The browser must fetch it. |
modulepreload | A module this page needs. | It fetches the module and gets it ready to run. | Yes. The browser must fetch it. |
prefetch | A 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.
- 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
Sort each case by the hint that fits it.
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.
What does the last line print?
// 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.`);In an incognito window, preconnect saved 0 ms. Chrome ignored the hints there, so the heading appeared as late as with none.
What does the last line print?
// 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.`);The prefetch made the list page 0 ms slower. Chrome sent the prefetch at a low urgency, so the list page's own needs went first.
With the font preloaded but no crossorigin, when did the heading appear?
1,368 ms, almost the same as with crossorigin. Here the font's own request found the file in the HTTP cache. Without the cache, the font would have crossed the link twice.
Which Priority header did the prefetch of product.js carry?
u=4, i: an urgency of 4, below the list page's own u=0, i. The i means the file is useful in pieces.
After how many seconds does the browser close a preconnected connection that wasn't used?
10 seconds. So preconnect only to servers the page will use soon.
About how long after the load event does Chrome warn about an unused preload?
About 3 seconds after the load event. That's the warning the gallery's two fonts got.
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?
Question 1 of 8What does line 6 of the font preload example print?
Read the code, then predict// 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.
Question 2 of 8What does line 7 of the prefetch example print?
Read the code, then predict// 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.
Question 3 of 8How is dns-prefetch different from preconnect?
Choose an answer to see the explanation.
Question 4 of 8A preconnect without crossorigin didn't speed up the font. Why?
Choose an answer to see the explanation.
Question 5 of 8The same preconnect did nothing in an incognito window. Why?
Choose an answer to see the explanation.
Question 6 of 8Why did modulepreload make the modules page paint sooner?
Choose an answer to see the explanation.
Question 7 of 8Two preloaded fonts the page never used made the hero appear later. Why?
Choose an answer to see the explanation.
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
crossoriginfor 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,typeandcrossorigin. 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:
- Font-display, MDN.
- HTML Standard: Link types, WHATWG.
- Establish network connections early to improve perceived page speed, web.dev.
- Speculative loading, MDN.
- Fetch Standard: Credentials, WHATWG.
- HTML Standard: Create a potential-CORS request, WHATWG.
- Fetch Standard: Connections, WHATWG.
- HTML Standard: Link type preconnect, WHATWG.
- chrome/browser/predictors/loading_predictor_config.cc, Chromium.
- chrome/browser/predictors/network_hints_handler_impl.cc, Chromium.
- Preload critical assets to improve loading speed, web.dev.
- Preload modules, web.dev.
- Prefetch resources to speed up future navigations, web.dev.
- RFC 9218: Extensible Prioritization Scheme for HTTP, the IETF.
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.