Connections in practice
Reuse connections, cut third-party origins, and make HTTP/2 and HTTP/3 prioritization work for you. Measured in Chrome over HTTP/2 on a slow link: a page with its font and library on other origins, host names that did and didn't share a connection, a script and a font that needed two, the Priority header on every request, a server that honoured it, and two sites that couldn't share a CDN copy.
- 01Count the connections a page needsOne for each origin, unless two host names can share one. A second one when some requests carry credentials and others don't.
- 02Cut the extra onesSelf-host what you can. Put your own host names at one address, behind one certificate. Add crossorigin so a script matches a font.
- 03Make priorities workRead the Priority header Chrome sends, nudge it with fetchpriority, and check that your server sends the urgent files first.
Fewer, shared connections
Asha opens the tea shop's spring sale page on her phone. The page is small. It has a heading in a handwritten font, one line of text, and a script library the shop uses. That makes four files: the page itself, a stylesheet that sets up the font, the font file, and the script.
Every file lives at an origin: the scheme, host name and port of its address, like https://shop.test. A different host name is a different origin. The shop could serve all four files from its own origin. That's called self-hosting.
But the shop's developer took a shortcut. They pasted one line from a font service, which brings in the stylesheet and the font. They pasted another from a public CDN: a server that hosts popular libraries for any site to link to. Now the files come from four different origins.
The bytes are the same either way. So does it matter where they come from? The course built both versions of the page and loaded each one 5 times in Chrome, on a slow line. Every message took 150 ms to reach the server and come back, as on Ravi's line in Latency vs bandwidth. Here is what it measured:
// The same sale page, two ways. LCP is the median of 5 runs, in ms.const selfHosted = { connections: 1, lcp: 1280 };const otherOrigins = { connections: 4, lcp: 1940 }; console.log(`Self-hosted: ${selfHosted.connections} connection, LCP at ${selfHosted.lcp} ms.`);console.log(`Font and library from other origins: ${otherOrigins.connections} connections, LCP at ${otherOrigins.lcp} ms.`);- Line 1 is a comment. The median is the middle value of the 5 runs. LCP, the largest contentful paint, is when the biggest thing on the screen was drawn. On this page, that's the heading in its handwritten font.
- Line 2 is the self-hosted page. It opened 1 connection, and its LCP came at 1,280 ms.
- Line 3 is the page with its font and library on other origins. It opened 4 connections, and its LCP came at 1,940 ms.
- Lines 5 and 6 print
Self-hosted: 1 connection, LCP at 1280 ms.andFont and library from other origins: 4 connections, LCP at 1940 ms.
Same bytes, but the heading appeared 660 ms later. The files didn't change. Only the number of places they came from did, and each place cost a connection. Here is the whole difference in one picture:
Why does each extra place cost so much? Think of a shopping trip.
Your errand list says tea, bread and birthday candles. If one shop sold all three, you'd queue once and be done. Instead, each shop has its own door and its own queue, and you can't ask for anything until you reach the front. Worse, the baker tells you the candles are sold next door. That queue can't even start until you're done at the bakery.
- In real life: Another shop on your list
- In JavaScript: Another origin the page needs
- In real life: Finding the shop, then queueing at its door
- In JavaScript: A DNS lookup, then a new connection's handshakes
- In real life: One shop that sells everything
- In JavaScript: Every file on one origin: self-hosting
- In real life: "The candles are sold next door"
- In JavaScript: fonts.test's stylesheet, naming files.fonts.test
Each origin a page uses needs its own connection. Every new connection spends round trips on handshakes before it can carry a single request. So use fewer origins, let your own host names share a connection, and make sure the server sends the most urgent bytes first.
The rest of the lesson takes this apart, one question at a time:
- How much does each new origin cost, and why do some cost twice?
- When can two host names share one connection?
- Why does one origin sometimes need two connections?
- Once files share a connection, whose bytes go first?
- Is a public CDN's copy still worth a connection?
Every page the course recorded is in this playground. Pick one to see each connection it opened, and every file that went over it:
- FCP1,076 ms
- LCP1,940 ms
- New connections4
- Connection 1, to shop.test/third-party (u=0, i)
- Connection 2, to fonts.test/css?family=Caveat (u=0)
- Connection 3, to cdn.test/lib.js (u=1)
- Connection 4, to files.fonts.test/caveat.woff2 (u=0)
The same page, with the font stylesheet from fonts.test, the font from files.fonts.test and the library from cdn.test. Each of those lives at its own address. The times are medians of 5 runs. The connections and requests were the same in every run. The brackets hold each request's Priority header.
The first two rows are times. FCP, the first contentful paint, is when the first text or picture appeared. Then comes LCP. After that, there's one row for each connection, named for the host the browser asked for. The brackets hold each request's Priority header, which the priorities section explains.
The cost of each new origin
A connection leads to one origin. On the self-hosted page, every file came from https://shop.test, so one connection carried all four. On the other page, each new origin needed a connection of its own.
A new connection can't carry anything straight away. First it has to finish two handshakes: short exchanges of messages that set something up before any data flows. Module 4 counted them. TCP's hello takes one round trip, as TCP connections and slow start showed. TLS 1.3's secret code takes another, as in TLS without the wait. On this line, that comes to:
// Before a new connection can carry its first request, on this line.const roundTripMs = 150;const handshakes = 2; // TCP's hello, then TLS 1.3's secret code console.log(`One new connection: ${handshakes * roundTripMs} ms of handshakes.`);console.log(`Two new connections, one after the other: ${2 * handshakes * roundTripMs} ms.`);- Line 2 is the round trip on this line: 150 ms.
- Line 3 counts the handshakes. Each one takes a round trip.
- Line 5 multiplies the two and prints
One new connection: 300 ms of handshakes. - Line 6 doubles that, for two connections opened one after the other. It prints
Two new connections, one after the other: 600 ms.
You can see the first number in the recordings. In every setup, the page's own first byte came at about 479 ms. That's just over three round trips: two for the handshakes, and one to ask for the page and hear back.
But why would two new connections ever come one after the other? Because the browser can't connect to a host it hasn't heard of yet. Here is where it first read each host name on the page with other origins:
// Where the browser first read each host name, on the page with other origins.const foundIn = { "shop.test": "the address Asha opened", "fonts.test": "named in the page's HTML", "cdn.test": "named in the page's HTML", "files.fonts.test": "named in the stylesheet from fonts.test",}; for (const [host, place] of Object.entries(foundIn)) console.log(`${host}: ${place}.`);- Line 3 is the page's own host. The browser knew it from the start.
- Lines 4 and 5 are the font service and the CDN. The page's HTML names both, so the browser found them as soon as the page arrived.
- Line 6 is the host the font file lives on. Only the stylesheet from fonts.test names it.
- Line 9 prints one sentence for each host, like
files.fonts.test: named in the stylesheet from fonts.test.
So the font waited in a chain. The browser opened a connection to fonts.test and downloaded the stylesheet. Only then did it learn about files.fonts.test, and open a second connection there. web.dev warns about this. A font provider "may serve stylesheets and fonts from separate origins", and its font files need "separate connection setups."
That's two new connections in a row: 600 ms of handshakes before the font could even be asked for. The heading came 660 ms later than on the self-hosted page.
Even the first paint moved, from 748 ms to 1,076 ms. The library is a plain script in the page's head. The browser stops reading the HTML until such a script has run, so nothing below it can be drawn. Render-blocking and parser-blocking resources showed why. Now that script came over a new connection, so the first paint waited for its handshakes too.
One cost is missing from these recordings. Here, every host name was looked up instantly, so no page paid for DNS. A real visitor's browser has to look up each new host name first, as DNS lookups showed. That's one more wait for every new origin.
Why it matters: every origin you add puts handshakes in front of its files. When a file is needed for the first view, like a font or a blocking script, those handshakes land straight on FCP or LCP. The simplest fix is to serve those files from your own origin.
Connection reuse and coalescing
The self-hosted page shows the simplest kind of sharing. Its one connection to shop.test carried the page, then the stylesheet, the script and the font. That's connection reuse. Once a connection is open, later requests to the same origin use it, and skip the handshakes. It's the phone call from TCP connections and slow start that stays connected, so you can ask all your questions on one call.
Now a harder case. A shop may serve its pictures from a second host name of its own, like img.shop.test. That's a different origin. Does it need a new connection?
Not always. A browser may send a second host name's requests over a connection it already has. That's connection coalescing. HTTP/1.1 and HTTP/2 pictured it as two shops behind one door. RFC 9113, the HTTP/2 standard, allows it. For an https:// address, the browser checks two things:
- The host name leads to the same IP address as the open connection. An IP address is the number computers use to find each other.
- The open connection's certificate is valid for the host name. A certificate is the server's proof of the names it may answer for. In RFC 9113's words, reuse "additionally depends on having a certificate that is valid for the host in the URI."
The tea shop has grown into a supermarket. Its tea counter and its bakery counter are in the same building, and the sign at the entrance names them both. Once you're inside, you just walk from one counter to the other. But a counter with its own street door and its own sign means going back out and queueing again. So does the branch across town, even though the main sign lists it too.
- In real life: The supermarket's building
- In JavaScript: One IP address
- In real life: The sign at the entrance, naming every counter
- In JavaScript: The shop's certificate, naming img.shop.test and media.shop.test
- In real life: Walking to the bakery counter inside
- In JavaScript: img.shop.test's request, sent on the page's open connection
- In real life: A counter with its own street door and sign
- In JavaScript: static.shop.test, with a certificate of its own
- In real life: The branch across town
- In JavaScript: media.shop.test, at another address
The course put this to the test. It ran each address as a server of its own, and served the page's hero, the big photo at the top, from three host names. On these pages, the hero is the biggest thing on the screen, so LCP waits for it. Read each row as two questions: is it in the same building, and is it on the sign?
| Host name | Same address as shop.test? | Named on the shop's certificate? |
|---|---|---|
img.shop.test | Yes. | Yes. |
static.shop.test | Yes. | No. It has a certificate of its own. |
media.shop.test | No. It lives at another address. | Yes. |
Here are the first two, as the course measured them:
// The hero photo from two other host names. LCP is the median of 5 runs, in ms.const fromImg = { connections: 1, lcp: 1160 };const fromStatic = { connections: 2, lcp: 1488 }; console.log(`Hero from img.shop.test: ${fromImg.connections} connection in all, LCP at ${fromImg.lcp} ms.`);console.log(`Hero from static.shop.test: ${fromStatic.connections} connections in all, LCP at ${fromStatic.lcp} ms.`);- Line 2 is img.shop.test. The page opened 1 connection in all, so the hero came over the page's own connection.
- Line 3 is static.shop.test. The page opened 2 connections: its own, and one more just for the hero.
- Lines 5 and 6 print
Hero from img.shop.test: 1 connection in all, LCP at 1160 ms.andHero from static.shop.test: 2 connections in all, LCP at 1488 ms.
The shared connection showed the hero 328 ms sooner. That's about the 300 ms of handshakes a new connection costs on this line.
The picture's third row is media.shop.test. The shop's certificate names it, but it lives at another address. One missing condition was enough: it needed a connection of its own too.
Why it matters: if you serve files from host names of your own, put them at the same address, behind one certificate that names them all. Then they cost no new connection. A font service or a public CDN can't join in this way, because your certificate doesn't name its host.
crossorigin and separate connection pools
Here's a surprise: one origin doesn't always mean one connection. The course loaded a page that takes a font and a script from cdn.test:
<style> @font-face { font-family: Caveat; src: url(https://cdn.test/caveat.woff2) format("woff2"); font-display: block; }</style><script src="https://cdn.test/lib.js"></script>- Line 2 tells the browser where the font lives: on cdn.test.
- Line 4 loads the script from cdn.test too. It's a plain
<script>tag.
The browser opened two connections to cdn.test: one for the script, and one for the font. Why didn't they share?
Because a connection carries more than one label. The Fetch Standard, which describes how browsers fetch, says: "Each connection is identified by an associated key (a network partition key), origin (an origin), and credentials (a boolean)." A boolean is a true-or-false value. So every connection is labelled with three things:
- the site the page is on, called its network partition key (the last section comes back to it);
- the origin it leads to;
- whether its requests carry credentials.
Credentials are things that tell a server who you are, like cookies. A request either carries them or it doesn't. A request with credentials can only use a connection labelled "with". A request without them can only use one labelled "without". Now look at the two files again:
- A plain script tag asks with credentials. The HTML standard sets its request's credentials mode to "include".
- A font asks without them. The CSS standards fetch every font in CORS mode, with credentials only for the page's own origin. CORS, cross-origin resource sharing, is the set of rules that lets a server say which other origins may read its files.
So the script needed a connection "with", and the font needed one "without". Two labels, two connections.
A wholesale shop has two doors. Members show their card at one. Guests, with no card, go in at the other. Each door has its own queue. You can only share a trip with a friend if you both use the same door. If you show your card and your friend has none, you queue twice at the same shop.
- In real life: The members' door, where you show your card
- In JavaScript: A connection for requests with credentials
- In real life: The guests' door, with no card
- In JavaScript: A connection for requests without credentials
- In real life: You, showing your card
- In JavaScript: The plain script tag
- In real life: Your friend, who never has a card
- In JavaScript: The font
- In real life: Leaving your card in your pocket
- In JavaScript: Adding crossorigin to the script
The fix is one word. The crossorigin attribute asks for the script the way a font is asked for: in CORS mode, with no credentials for another origin.
<script src="https://cdn.test/lib.js" crossorigin></script>- Line 1 is the same script tag, with
crossoriginadded. Now the script and the font carry the same label.
The course loaded both versions of the page:
// A font and a script from cdn.test. LCP is the median of 5 runs, in ms.const plainScript = { toCdn: 2, lcp: 1920 };const withCrossorigin = { toCdn: 1, lcp: 1596 }; console.log(`Plain script: ${plainScript.toCdn} connections to cdn.test, LCP at ${plainScript.lcp} ms.`);console.log(`Script with crossorigin: ${withCrossorigin.toCdn} connection to cdn.test, LCP at ${withCrossorigin.lcp} ms.`);- Line 2 is the plain script: 2 connections to cdn.test, and LCP at 1,920 ms.
- Line 3 is the script with crossorigin: 1 connection to cdn.test, and LCP at 1,596 ms.
- Lines 5 and 6 print
Plain script: 2 connections to cdn.test, LCP at 1920 ms.andScript with crossorigin: 1 connection to cdn.test, LCP at 1596 ms.
The heading appeared 324 ms sooner. That's close to the 300 ms of handshakes the font no longer had to wait for: it used the connection the script had already opened.
Two things to check before you add it:
- The server has to allow it. A CORS request needs an
Access-Control-Allow-Originheader in the answer, or the browser won't use the file. cdn.test sentAccess-Control-Allow-Origin: *, which allows every origin. - The script's request now goes without cookies. If the server needs them to answer, the script needs its connection "with".
Why it matters: the same labels explain an easy mistake with preconnect, a hint that opens a connection early. The HTML standard opens a preconnect's connection "with" credentials unless it has crossorigin. So a preconnect for a font host needs crossorigin, or the font can't use it. Resource hints, next, shows it.
Extensible priorities
Now say all of a page's files share one connection. Good. But the line still has one speed. When several files are on their way at once, whose bytes go first?
The server decides, because it's the one sending. But it can't see the page, so the browser tells it what matters most. HTTP/1.1 and HTTP/2 pictured this as telling a café kitchen "the tea first, the cake last." Today Chrome says it with a Priority header on each request.
The header comes from RFC 9218, the Extensible Prioritization Scheme. Its format is "designed to provide future extensibility": more parameters can be added later. RFC 9218 itself defines two:
uis the urgency: a number from 0 to 7. The smaller the number, the sooner the file is wanted. RFC 9218 adds: "The default is 3."imeans incremental: the file is useful in pieces. The RFC says such a file can "provide some meaningful output as chunks of the response arrive." Withouti, the browser only uses the file once it's complete.
The course built a gallery page to see these headers. It has the sale heading, a stylesheet, a font, a hero photo marked fetchpriority="high", four more photos, and two small scripts. Here is what Chrome sent:
// The Priority header Chrome sent with the gallery's files, the same in every run.const sent = { "the page": "u=0, i", "stylesheet": "u=0", "font": "u=0", "hero photo, fetchpriority=high": "u=1, i", "other photos": "u=2, i",}; for (const [file, priority] of Object.entries(sent)) console.log(`${file}: ${priority}`);- Line 2 starts a list of the gallery's files, each with the header Chrome sent. They were the same in every run.
- Lines 3 to 5 are the page, its stylesheet and its font. All three got urgency 0, the most urgent. The page is also marked
i. - Lines 6 and 7 are the photos. The hero got urgency 1, and the other four got 2. All of them are marked
i. - Line 10 prints one line for each file, from
the page: u=0, itoother photos: u=2, i.
Chrome picks these numbers itself, the way How browsers prioritize requests showed. You don't have to set anything up. Two files went without a header at all:
// Two scripts on the gallery page. Chrome sent no Priority header with either.const sent = { "app.js, defer": null, "analytics.js, async": null,}; for (const [file, priority] of Object.entries(sent)) console.log(`${file}: ${priority ?? "no header, so urgency 3"}`);- Lines 3 and 4 are the gallery's two scripts, one loaded with
deferand one withasync. The valuenullmeans no header was sent. - Line 7 prints each one. When the value is
null,??puts in the words instead. It printsapp.js, defer: no header, so urgency 3andanalytics.js, async: no header, so urgency 3.
No header means the defaults. RFC 9218 says a server "SHOULD act as if their default values were specified." So the two scripts sit at urgency 3, behind every photo.
But a header is only a request. RFC 9218 is plain about it: "Expressing priority is only a suggestion." It recommends that servers send "higher-urgency responses before lower-urgency responses." A server doesn't have to.
Five friends are waiting for tea, and you have one teapot. You could pour a splash into each cup in turn. Then all five cups fill slowly together, and nobody can drink until near the end. Or you could fill the cups of the friends in a hurry first, then pour for the rest.
- In real life: The one teapot, pouring at its own speed
- In JavaScript: One connection, at the line's speed
- In real life: A splash in each cup, in turn
- In JavaScript: A server that sends a piece of each file in turn
- In real life: Filling the hurried friends' cups first
- In JavaScript: A server that sends by urgency, as RFC 9218 recommends
- In real life: The friends in a hurry
- In JavaScript: Files with a small urgency number, like the hero photo
The course built two servers for the gallery to see the difference. Both got the same requests, with the same headers. Both sent each file in pieces of 16 KB, one piece whenever the line had room. They differed only in which file's piece went next:
- The first shared the line evenly. It took the waiting files in turn, a piece of each.
- The second followed RFC 9218. It sent the files with the smallest urgency number first. Files at the same urgency marked
itook turns.
// The gallery's LCP, median of 5 runs, in ms. Same requests, same headers, two servers.const sharesEvenly = 2572;const sendsByUrgency = 1608; console.log(`Server that shares the line evenly: LCP at ${sharesEvenly} ms.`);console.log(`Server that sends by urgency: LCP at ${sendsByUrgency} ms.`);- Line 2 is the server that shared the line evenly: LCP at 2,572 ms.
- Line 3 is the server that sent by urgency: LCP at 1,608 ms.
- Lines 5 and 6 print
Server that shares the line evenly: LCP at 2572 ms.andServer that sends by urgency: LCP at 1608 ms.
Same browser, same requests, same headers. The hero appeared 964 ms sooner, only because the server sent it before the other photos. With even sharing, the hero shared the line with every other waiting file, a piece at a time, so it finished late.
Why it matters: Chrome already sends these hints by itself. Whether they help depends on your server or CDN, so check that it acts on the Priority header. If the browser guesses wrong about a file, correct it with fetchpriority. The header isn't tied to HTTP/2 either. RFC 9218 calls it a signal "independent of protocol version", so it works over HTTP/3 too.
Self-hosting vs third-party CDNs
Back to the shop's shortcut. A file from a font service or a public CDN is third-party: it comes from someone else's origin. There used to be a good reason to link one.
Chrome used to file each cached copy under its URL alone. So if many sites linked the same library, a visitor who had been to one of them already had it. Then Chrome split its cache by site. The browser's caches pictured that as each school keeping its own lost-property box. Chrome's post on the change warned that services sharing "fonts and popular scripts" across many sites "may see an increase in their traffic."
Connections are split the same way. Remember the first label on every connection, its network partition key? The Fetch Standard defines it as "a tuple consisting of a site and null or an implementation-defined value." A tuple is a short list of values. In plain words, the site you're on is part of the label.
The course tested it with one browser. It visited site A, then site B. Both pages load https://cdn.test/lib.js, and no connection was closed in between:
// Site A, then site B, in one browser, with no connection closed in between.// Both load https://cdn.test/lib.js. What reached cdn.test, the same in every run:const siteA = { requests: 1, newConnections: 1 };const siteB = { requests: 1, newConnections: 1 }; console.log(`Site A: ${siteA.requests} request for lib.js, ${siteA.newConnections} new connection to cdn.test.`);console.log(`Site B: ${siteB.requests} request for lib.js, ${siteB.newConnections} new connection to cdn.test.`);- Lines 1 and 2 are comments: what happened, and what was counted at cdn.test.
- Line 3 is site A: 1 request for lib.js reached cdn.test, over 1 new connection.
- Line 4 is site B: exactly the same.
- Lines 6 and 7 print
Site A: 1 request for lib.js, 1 new connection to cdn.test.andSite B: 1 request for lib.js, 1 new connection to cdn.test.
Site B got nothing from site A's visit: not the file, and not the connection. It asked cdn.test again, over a new connection of its own. So for a library, a public CDN's copy saves nothing any more. Self-hosting it loses no cache hits, and it saves a connection.
web.dev adds a warning about fonts: "In practice, the performance differences between these two options is less clear cut." Before you self-host fonts, it says to "confirm that your site uses a Content Delivery Network (CDN) and HTTP/2." Then measure. Compare "the transfer time (including connection setup)" of your own copy with the font service's.
Why it matters: a third-party file costs a connection, and another site's copy no longer saves the download. Self-host it, serve it from a CDN of your own, and check the result.
What would you change?
Each case below costs a connection the page doesn't need. Sort it by the change that saves it:
- A web font from a font service
- A library from a public CDN
- A script and a font from your own CDN host, with a plain script tag
- A preconnect to the host your font files come from
- img.example.com on a certificate of its own, at the same address as www
- static.example.com at another IP address, with a certificate that names both
Sort each case by the change that saves a connection.
Common mix-ups
- "A popular CDN's copy is probably in the cache already." No. Caches are split by site. Site B downloaded lib.js again, over a new connection.
- "My own subdomains are free." No. static.shop.test had a certificate of its own, so it needed a connection of its own. LCP came at 1,488 ms instead of 1,160.
- "One origin means one connection." No. A plain script and a font from cdn.test needed two: one with credentials, one without.
- "The browser decides the order bytes arrive in." No. The browser only asks. The server decides. The same gallery had its LCP at 2,572 ms or 1,608, depending on the server.
- "I have to set up priorities before Chrome sends them." No. Chrome sent a Priority header with the gallery's files by itself. Only the defer and async scripts went without one.
Practice exercises
Answer first, then use the hints.
The course also served the hero from media.shop.test. What does the last line print?
// The hero photo from two other host names. LCP is the median of 5 runs, in ms.
const fromImg = { connections: 1, lcp: 1160 };
const fromStatic = { connections: 2, lcp: 1488 };
const fromMedia = { connections: 2, lcp: 1484 };
console.log(`Hero from img.shop.test: ${fromImg.connections} connection in all, LCP at ${fromImg.lcp} ms.`);
console.log(`Hero from static.shop.test: ${fromStatic.connections} connections in all, LCP at ${fromStatic.lcp} ms.`);
console.log(`Hero from media.shop.test: ${fromMedia.connections} connections in all, LCP at ${fromMedia.lcp} ms.`);Hero from media.shop.test: 2 connections in all, LCP at 1484 ms. The shop's certificate names media.shop.test, but it lives at another address. So the hero needed a connection of its own.
Line 8 adds a plain script from the self-hosted page's head, with the header Chrome sent. What does the last line print?
// The Priority header Chrome sent with the gallery's files, the same in every run.
const sent = {
"the page": "u=0, i",
"stylesheet": "u=0",
"font": "u=0",
"hero photo, fetchpriority=high": "u=1, i",
"other photos": "u=2, i",
"plain script in the head": "u=1",
};
for (const [file, priority] of Object.entries(sent)) console.log(`${file}: ${priority}`);plain script in the head: u=1. Chrome sent urgency 1 with it: less urgent than the stylesheet, more urgent than the photos.
The gallery opened just one connection. How many requests went over it?
11: the page, the stylesheet, the font, two scripts, a request for stock levels, and five photos. One connection carried every one of them.
What was site B's FCP?
1,080 ms. Site A's was 1,068 ms. Site B was no faster, because it shared nothing with site A.
A server doesn't want a request that came over a shared connection. Which status code does it answer with?
421 Misdirected Request. The server says it won't answer for that host on this connection. RFC 9110 lets the browser send the request again, over a different connection.
Starting with which version did Chrome split its HTTP cache by site?
Chrome 86.
Check your understanding
For each question, ask three things: how many connections each request needed, which requests could share one, and in what order the server sent the bytes.
Question 1 of 6What does the second line of this example print?
Read the code, then predict// The same sale page, two ways. LCP is the median of 5 runs, in ms. const selfHosted = { connections: 1, lcp: 1280 }; const otherOrigins = { connections: 4, lcp: 1940 }; console.log(`Self-hosted: ${selfHosted.connections} connection, LCP at ${selfHosted.lcp} ms.`); console.log(`Font and library from other origins: ${otherOrigins.connections} connections, LCP at ${otherOrigins.lcp} ms.`);Choose an answer to see the explanation.
Question 2 of 6What does the first line of this example print?
Read the code, then predict// The hero photo from two other host names. LCP is the median of 5 runs, in ms. const fromImg = { connections: 1, lcp: 1160 }; const fromStatic = { connections: 2, lcp: 1488 }; console.log(`Hero from img.shop.test: ${fromImg.connections} connection in all, LCP at ${fromImg.lcp} ms.`); console.log(`Hero from static.shop.test: ${fromStatic.connections} connections in all, LCP at ${fromStatic.lcp} ms.`);Choose an answer to see the explanation.
Question 3 of 6static.shop.test was at the shop's own address, but it didn't share the page's connection. Why?
Choose an answer to see the explanation.
Question 4 of 6Why did crossorigin on the script save a connection to cdn.test?
Choose an answer to see the explanation.
Question 5 of 6The two gallery servers got the same requests and the same Priority headers. Why did one show the hero much sooner?
Choose an answer to see the explanation.
Question 6 of 6Site B loaded the same CDN file as site A, in the same browser. Why did it download it again?
Choose an answer to see the explanation.
Key takeaways
- Each origin costs a connection, and each new connection spends two round trips on handshakes. With its font and library on other origins, the sale page's LCP went from 1,280 ms to 1,940.
- A host name can share a connection only at the same address, with a certificate that names it.
- Requests with and without credentials need separate connections. Add crossorigin so a script matches a font.
- Chrome sends a Priority header with most requests. Make sure your server or CDN acts on it.
- Caches and connections are split by site, so a public CDN's copy is never shared. Self-host what you can, then measure.
Remember the one-liner.
Fewer origins, shared connections, and a server that sends the urgent bytes first.
Sources
The facts on this page come from these standards and docs:
- Best practices for fonts, web.dev.
- RFC 9113: HTTP/2, section 9.1.1, Connection Reuse, the IETF.
- Fetch Standard: Connections, WHATWG.
- HTML Standard: create a potential-CORS request, WHATWG.
- CSS Fonts Module Level 4: font fetching requirements, W3C.
- CSS Values and Units Module Level 4: fetch a style resource, W3C.
- HTML Standard: the preconnect link type, WHATWG.
- RFC 9218: Extensible Prioritization Scheme for HTTP, the IETF.
- Gaining security and privacy by partitioning the cache, Chrome for Developers.
- RFC 9110: HTTP Semantics, 421 Misdirected Request, the IETF.
Coming next: Resource hints, also in Fast servers, caches, and networks. dns-prefetch, preconnect, preload, modulepreload and prefetch: what each does, and when it backfires.