HTTP/1.1 and HTTP/2
HTTP/1.1 sends one request at a time per connection; HTTP/2 sends them all on one. See real loads of both, and which old tricks to unlearn.
- 01Explain why HTTP/1.1 loads in wavesSay why one HTTP/1.1 connection carries one request at a time, and why browsers open six of them.
- 02Explain how HTTP/2 shares one connectionDescribe streams, frames and header compression, how the browser says what matters first, and when two hostnames share a connection.
- 03Choose which old tricks to dropDecide whether sharding, sprites, bundling and inlining still help, and use Early Hints where server push used to go.
Many files, few connections
Ravi opens a page at Leaf and Kettle, the tea shop from the first lessons. It shows every tea the shop sells: 36 of them, each with a small picture of its cup. The last three lessons followed the browser up to this point. It found the server, opened a line to it, and made the line private. Now the line is ready, and the browser asks for the page.
The page comes back. But it's mostly a list of other files to fetch: one stylesheet and 36 pictures. That's 37 more requests. How do they share the line?
That depends on the version of HTTP, the language the browser and the server speak. The browser sends a request: "please send me assam.svg". The server sends back a response: the file. This lesson compares two versions, the older HTTP/1.1 and the newer HTTP/2.
The course loaded the page twice, on a line like Ravi's. On that line, every round trip took 150 ms. A round trip is a message going to the server and an answer coming back. Same page, same pictures, same line. Only the version of HTTP changed:
// When the last of the 36 tea pictures arrived, in ms from the start of the visit.const lastPicture = { http1: 1279, http2: 347 }; console.log(`Over HTTP/1.1, the last picture arrived at ${lastPicture.http1} ms.`);console.log(`Over HTTP/2, it arrived at ${lastPicture.http2} ms.`);- Line 1 is a comment. The times are in milliseconds (ms), counted from the start of the visit. A millisecond is a thousandth of a second.
- Line 2 holds two recorded times, one for each version of HTTP.
- Lines 4 and 5 print
Over HTTP/1.1, the last picture arrived at 1279 ms.andOver HTTP/2, it arrived at 347 ms.
HTTP/2 finished 932 ms sooner. That's almost a whole second, on the same line, for the same pictures. Where did the second go? Here is the whole answer in one picture:
Read the top half first. HTTP/1.1 opens six connections, and each one carries one request at a time. Requests 7 to 12 wait for 1 to 6 to finish. So the 37 requests go in seven waves, and each wave costs a round trip.
The bottom half is HTTP/2. One connection carries all 37 requests at once, in a single round trip.
Why does HTTP/1.1 need six connections? Think of a supermarket:
At the supermarket, one checkout lane serves one shopper at a time. You only have a bottle of milk, but the shopper in front has a full trolley, so you wait. The shop opens six lanes. Now six shoppers pay at once. But the seventh still waits for a lane to free up.
- In real life: A checkout lane
- In JavaScript: One HTTP/1.1 connection, carrying one request at a time
- In real life: Waiting behind a full trolley
- In JavaScript: Head-of-line blocking: a slow response holds up the next request
- In real life: Six lanes open
- In JavaScript: The browser's six connections to one host
- In real life: The seventh shopper, still waiting
- In JavaScript: The seventh request, waiting for a free connection
HTTP/2 doesn't open more connections. It changes how one connection is shared:
- HTTP/1.1 carries one request and its response at a time on each connection. So browsers open up to six connections to each host.
- HTTP/2 uses one connection to each host. Every request travels on it at the same time. It also shrinks the notes, called headers, that ride along with every request.
HTTP/1.1 sends many files in waves, six at a time. HTTP/2 sends them all at once, on one connection. So a page with many small files saves round trips on HTTP/2. And the old tricks that worked around six connections, like sharding and sprites, now mostly get in the way.
The system design lesson HTTP/1.1, HTTP/2, and HTTP/3 compares the versions side by side. This lesson watches two of them load the same page.
Head-of-line blocking in HTTP/1.1
Why can one HTTP/1.1 connection carry only one request at a time? Because of the way it talks. The browser sends a request, then waits for the whole response. Only when the last byte has arrived can it send the next request on that connection.
So a slow response holds up every request behind it, like the full trolley at the checkout. That's called head-of-line blocking: whoever is at the head of the line blocks everyone behind.
HTTP/1.1 did have a fix on paper, called pipelining. The browser could send several requests without waiting. But the server still had to send the responses back in the same order. So one slow response still held up the rest. MDN notes that no modern browser turns pipelining on by default.
So browsers do the other thing they can: they open more connections. Ilya Grigorik, in his book High Performance Browser Networking, writes that browsers "open up to six connections per host." A host, or hostname, is one server name, like completefrontend.com.
Why six, and not sixty? Every new connection pays for its own TCP and TLS handshakes, as the last two lessons showed. Grigorik calls six "simply a safe middle ground."
Now count the waves for the tea page. Six requests go at once, and each wave takes one round trip:
const requests = 37; // the stylesheet and 36 picturesconst atOnce = 6; // six connections, one request on eachconst rttMs = 150; // one round trip on Ravi's line const waves = Math.ceil(requests / atOnce);console.log(`HTTP/1.1 needs ${waves} waves: about ${waves * rttMs} ms.`);- Line 1 is the 37 requests: the stylesheet and 36 pictures.
- Line 2 is how many can go at once: six, one on each connection.
- Line 3 is one round trip on Ravi's line, 150 ms.
- Line 5 divides the requests by six and rounds up, with
Math.ceil. Six waves carry 36 requests, so the 37th needs a seventh wave. - Line 6 prints
HTTP/1.1 needs 7 waves: about 1050 ms.
That's a simple model. Did the real load agree? Here is the recorded HTTP/1.1 load, counted from when the HTML finished:
// The HTTP/1.1 load, in ms from the start of the visit.const load = { htmlDone: 163, lastPicture: 1279 };const rttMs = 150; const afterHtml = load.lastPicture - load.htmlDone;console.log(`The last picture came ${afterHtml} ms after the HTML.`);console.log(`That's about ${Math.round(afterHtml / rttMs)} round trips.`);- Line 2 holds two recorded times: when the HTML finished, and when the last picture finished.
- Line 5 subtracts them. Line 6 prints
The last picture came 1116 ms after the HTML. - Line 7 divides that by the round trip. It prints
That's about 7 round trips.
Seven waves in the model, seven round trips in the real load. The pictures really did come in waves: 5, then 6, 6, 6, 6, 6, and 1. Why only 5 in the first wave? The stylesheet took the sixth connection.
You can watch the waves arrive. Chrome took these pictures of the screen while the page loaded:
- Nothing drawn yet
0 ms to 347 msA blank screen

At 347 msThe header and title

At 365 ms5 of 36 pictures in

At 493 ms11 of 36 pictures in

At 654 ms17 of 36 pictures in

At 813 ms23 of 36 pictures in
Why does this matter for your site? On HTTP/1.1, every six more files can cost another round trip, however small the files are. On a slow line like Ravi's, each of those round trips is 150 ms.
Multiplexing, streams, and header compression
One connection, many streams
HTTP/2 came out in 2015, as RFC 7540, and was revised in 2022, as RFC 9113. An RFC is one of the documents that define how the internet works. HTTP/2 keeps the meaning of every request and response. It changes only how they travel. Picture a waiter in a busy café:
A waiter carries one tray out of the kitchen. On it are a soup for table 4, a tea for table 7, and bread for table 4 again. Each dish has a ticket with its table number. The waiter doesn't finish table 4's whole order before bringing table 7 anything. Every trip carries a bit of everyone's order.
- In real life: The waiter's trips from the kitchen
- In JavaScript: The one connection
- In real life: One table's whole order
- In JavaScript: A stream: one request and its response
- In real life: One dish on the tray
- In JavaScript: A frame: one small piece of a stream
- In real life: The table number on each dish
- In JavaScript: The stream number on every frame
HTTP/2 cuts each request and each response into small pieces, called frames. One request and its response, with all their frames, make a stream. Every frame carries its stream's number. The browser numbers its streams with odd numbers: 1, 3, 5 and so on.
Frames from many streams take turns on the same connection. Mixing them like this is called multiplexing. The browser reads the number on each frame, and puts each file back together:
So no request waits for a free connection. Grigorik puts it this way: "only one connection per origin is required." An origin is the start of an address: its scheme, like https://, then the host, then a port number if there is one. For example, https://completefrontend.com.
How many streams can be open at once? Each server sets its own limit, and RFC 9113 recommends at least 100. Put that into the waves sum:
const requests = 37;const atOnce = 100; // streams at once: servers should allow at least 100const rttMs = 150; const waves = Math.ceil(requests / atOnce);console.log(`HTTP/2 needs ${waves} ${waves === 1 ? "wave" : "waves"}: about ${waves * rttMs} ms.`);- Line 1 is the same 37 requests.
- Line 2 is how many can go at once: 100 streams on one connection.
- Line 5 divides and rounds up, as before. All 37 requests fit in one wave.
- Line 6 says "wave" when there's only one. It prints
HTTP/2 needs 1 wave: about 150 ms.
And here is the real HTTP/2 load:
// The HTTP/2 load, in ms from the start of the visit.const load = { htmlDone: 160, lastPicture: 347 }; const afterHtml = load.lastPicture - load.htmlDone;console.log(`The last picture came ${afterHtml} ms after the HTML.`);- Line 2 holds the recorded times for the HTTP/2 load.
- Line 4 subtracts them. Line 5 prints
The last picture came 187 ms after the HTML.
187 ms is about one round trip of 150 ms. Every picture was asked for at once, so every picture came back at once. Chrome confirmed the version each time. It reported http/1.1 for the first load and h2 for this one. h2 is the short name for HTTP/2.
- Nothing drawn yet
0 ms to 345 msA blank screen

At 345 msThe header and title

At 362 ms36 of 36 pictures in
Try the recorded loads yourself. The first row of buttons picks the version. The second swaps the 36 picture files for one sprite, a single image holding every picture, which a later section explains:
http/1.1375 + 6 + 6 + 6 + 6 + 6 + 11,279 ms7 round tripsHTTP/1.1, 36 picture files: 37 requests, the stylesheet and the pictures, but only six connections, each carrying one request at a time. So the pictures came in 7 waves of 5, 6, 6, 6, 6, 6, 1. The last arrived at 1,279 ms, 7 round trips after the HTML.
Why does this matter? On HTTP/2, many small files are cheap to ask for, because they all go at once. So the first thing to check is that your server speaks HTTP/2.
Headers: writing "same as before"
HTTP/1.1 had a second cost, hidden in every request. Each request carries a few lines of notes on top, called headers. They say which site the browser wants, which browser it is, and what kinds of file it accepts. For each picture, Chrome sent the same 13 headers, with names like Host, User-Agent and Accept.
HTTP/1.1 sends them as plain text, in full, with every request. Grigorik counts 500 to 800 bytes each time, "and sometimes kilobytes more if HTTP cookies are being used." A cookie is a small note a site keeps in your browser, which goes back to the site with each request.
You post a letter to Ravi every day, through the same courier. On the first day, you write his whole address: name, house, street, town and postcode. The courier copies it into a notebook. From the second day on, you just write "same as before", and the courier looks it up.
- In real life: The whole address on the first letter
- In JavaScript: The first request's headers, sent in full
- In real life: The courier's notebook
- In JavaScript: HPACK's list of the headers already sent on this connection
- In real life: "Same as before"
- In JavaScript: A small number that points into that list
HTTP/2 compresses headers with a scheme called HPACK, from RFC 7541. It uses three tricks:
- A built-in list of 61 common headers, each with a number. A header on that list can go as just its number.
- A list of the headers already sent on this connection. A repeat goes as a number that points into it.
- A shorter code for the rest of the text, called Huffman coding. Common letters get shorter codes.
The course measured what that saves. It kept Chrome's own requests for the 36 pictures, as HTTP/1.1 text. Then Node's HTTP/2 client sent the same headers over one HTTP/2 connection. First, the HTTP/1.1 text:
// The headers of Chrome's 36 picture requests, sent as HTTP/1.1 text.const headerBytes = { first: 624, all: 22557 };const requests = 36; const eachAfter = Math.round((headerBytes.all - headerBytes.first) / (requests - 1));console.log(`The first request's headers took ${headerBytes.first} bytes.`);console.log(`Each request after it took about ${eachAfter} bytes.`);- Line 2 holds two measured numbers: the bytes of the first request's headers, and of all 36 requests together.
- Line 5 shares out the rest over the other 35 requests.
- Lines 6 and 7 print
The first request's headers took 624 bytes.andEach request after it took about 627 bytes.
Every request paid the full price again. Now the same headers over HTTP/2:
// The same headers, sent over one HTTP/2 connection.const headerBytes = { first: 407, all: 1616 };const requests = 36; const eachAfter = Math.round((headerBytes.all - headerBytes.first) / (requests - 1));console.log(`The first request's headers took ${headerBytes.first} bytes.`);console.log(`Each request after it took about ${eachAfter} bytes.`);- Line 2 holds the same two measurements, taken over HTTP/2.
- Lines 6 and 7 print
The first request's headers took 407 bytes.andEach request after it took about 35 bytes.
The first request still pays for its headers. After that, each repeat is mostly numbers pointing into the list. All 36 requests took 22,557 bytes of headers as HTTP/1.1 text, and 1,616 bytes over HTTP/2. And this page set no cookies.
Why does this matter? Headers ride on every request, so a page with many requests saves the most. Cookies make the gap wider, because HTTP/1.1 repeats them in full every time.
You can't see this saving from JavaScript, though. The browser keeps a record of how each file loaded, called Resource Timing. It reports a file's transferSize, its size on the network. In the recordings, every picture's transferSize was its own size plus exactly 300 bytes, on both versions. That's why the course measured the headers at the server.
Which version did this page you're reading come over? Press Look:
const files = performance.getEntriesByType("resource"); const protocols = Object.groupBy(files, (file) => file.nextHopProtocol || "hidden");const sent = (file) => file.requestStart || file.startTime;const onTheWire = (time) => files.filter((file) => sent(file) <= time && file.responseEnd > time).length;const busiest = Math.max(0, ...files.map((file) => onTheWire(sent(file))));console.log(protocols, busiest);Nothing read yet.
Press Look to see which protocol each of this page's files came over, and how many requests were on their way at the busiest moment.
In the panel's code:
- Line 3 groups the files by the version they came over, their
nextHopProtocol. Files from other sites may hide it. - Lines 4 to 6 find the busiest moment: when the most requests had been sent and hadn't finished yet.
Prioritization in HTTP/2
With six connections, the browser chose the order itself. Each time a connection was free, it sent the most important request next. On one shared connection, every request goes out at once. So who decides what comes back first?
The server does. But it can't see the page, so the browser has to tell it what matters most.
At a café, you order a tea, a soup and a slice of cake, all at once. You add: "The tea first, please, and the cake last." The kitchen cooks everything, but it sends the tea out first. A kitchen that didn't listen might bring the cake while your soup goes cold.
- In real life: Ordering everything at once
- In JavaScript: Sending every request on one connection
- In real life: "The tea first, the cake last"
- In JavaScript: The browser's priority signals
- In real life: The tea
- In JavaScript: The stylesheet, which the page needs before it can draw
- In real life: The cake
- In JavaScript: Pictures far below the screen
- In real life: A kitchen that listens
- In JavaScript: A server or CDN that follows the signals
Getting this right matters. RFC 9113 warns that with a poor scheme, HTTP/2's "performance could be significantly worse than HTTP/1.1."
HTTP/2 has had two ways for the browser to say what matters. Each row is one of them, and the last column says if it's used today:
| Which way | How does it work? | Is it used today? |
|---|---|---|
| A tree (RFC 7540, 2015) | Each request hangs below another one, with a weight from 1 to 256. A heavier weight gets a bigger share. | No. In 2022, RFC 9113 marked it as deprecated, which means no longer to be used. It was complex, and many servers ignored it. |
| An urgency (RFC 9218, 2022) | Each request gets a number from 0, the most urgent, to 7. The default is 3. A second flag says if the file is useful in pieces. | Yes. The browser sends it in a Priority header or a frame, and it works for HTTP/3 too. |
You don't send these signals yourself. The browser does. Your job is to make sure your server, or your CDN, follows them. A CDN is a network of servers around the world that sends your files from close to each visitor.
A later lesson shows the fetchpriority attribute. It lets you nudge the browser's choice for one file.
Connection coalescing
Many sites load files from several hostnames: www.example.com for the page, img.example.com for the pictures. Each new hostname normally needs its own DNS lookup, its own connection and its own TLS handshake. On Ravi's line, that can cost several round trips before the first picture is even asked for.
HTTP/2 can skip all of that. Picture two shops behind one door:
Leaf and Kettle shares its building with a bakery. There's one front door, and the sign above it lists both shops. You came in for tea, and now you want bread too. You don't walk out and come back in: you're already inside. For a shop in another building, you do have to walk over.
- In real life: The building's street address
- In JavaScript: The server's IP address
- In real life: The sign listing both shops
- In JavaScript: A certificate that covers both hostnames
- In real life: Staying inside for the bread
- In JavaScript: Sending img.example.com's requests on the open connection
- In real life: A shop in another building
- In JavaScript: A hostname at another IP address, which needs its own connection
A browser can send requests for a second hostname over a connection it already has. That's called connection coalescing. RFC 9113 allows it when the server is authoritative for the hostname, which means allowed to answer for it. For an https:// address, that takes two things:
- The hostname leads to the same IP address as the open connection.
- The connection's certificate covers the hostname. The certificate is the server's proof of the names it may answer for, as TLS without the wait showed.
Here is the check for img.example.com:
const connection = { ip: "203.0.113.10", certificate: ["www.example.com", "img.example.com"],};const img = { host: "img.example.com", ip: "203.0.113.10" }; const sameAddress = img.ip === connection.ip;const onCertificate = connection.certificate.includes(img.host);const shares = sameAddress && onCertificate;console.log(`${img.host} ${shares ? "shares the connection" : "needs its own connection"}.`);- Lines 1 to 4 are the open connection: its IP address, and the names on its certificate.
- Line 5 is img.example.com, which leads to the same address.
- Line 7 checks the address. Line 8 checks the certificate.
- Line 9 needs both to be true. Line 10 prints
img.example.com shares the connection.
If either check fails, the hostname needs a connection of its own:
const connection = { ip: "203.0.113.10", certificate: ["www.example.com", "img.example.com"],};const cdn = { host: "cdn.example.net", ip: "203.0.113.10" };const api = { host: "api.example.com", ip: "198.51.100.7" }; console.log(`Is cdn.example.net on the certificate? ${connection.certificate.includes(cdn.host)}`);console.log(`Is api.example.com at the same address? ${api.ip === connection.ip}`);- Line 5 is cdn.example.net. It's at the same address, but it isn't on the certificate.
- Line 6 is api.example.com. It's at a different address.
- Lines 8 and 9 print
Is cdn.example.net on the certificate? falseandIs api.example.com at the same address? false
A server can still say no. If it doesn't want a request sent this way, it answers 421 Misdirected Request, and the browser opens a new connection. The Web Almanac notes that browsers "can (and often do)" reuse connections like this.
Why does this matter? If you serve several hostnames from one address and one certificate, they can share a connection. And if a request to a new hostname shows no DNS lookup and no connection time, coalescing is the likely reason.
Domain sharding, spriting, and concatenation today
HTTP/1.1's six connections taught developers some tricks. Grigorik calls them "stopgap workarounds" for HTTP/1.1's limits. There were four:
- Domain sharding: spreading files across several hostnames, like img1. and img2., to get six more connections for each. It's like opening a second shop just to get six more checkout lanes.
- Spriting: drawing many small pictures into one image file, called a sprite. CSS then shows the right part of it in each place.
- Concatenation: joining many scripts, or many stylesheets, into one file. Today it's usually called bundling.
- Inlining: putting a small file's contents straight into the HTML, so it needs no request at all.
Do they still help? The course tested one. It recorded the tea page a second way, with all 36 pictures in one sprite. First, on HTTP/1.1:
// HTTP/1.1: when the last picture arrived, in ms.const lastPicture = { files: 1279, sprite: 498 }; console.log(`On HTTP/1.1, the sprite saved ${lastPicture.files - lastPicture.sprite} ms.`);- Line 2 holds two recorded times: when the last picture arrived as 36 files, and as one sprite.
- Line 4 subtracts them. It prints
On HTTP/1.1, the sprite saved 781 ms.
One request instead of 36 skipped all seven waves. On HTTP/1.1, the trick works. Now HTTP/2:
// HTTP/2: when the last picture arrived, in ms.const lastPicture = { files: 347, sprite: 495 }; console.log(`On HTTP/2, the sprite cost ${lastPicture.sprite - lastPicture.files} ms.`);- Line 2 holds the same two times, recorded over HTTP/2.
- Line 4 prints
On HTTP/2, the sprite cost 148 ms.
On HTTP/2, there were no waves to skip. And the sprite started late. It isn't named in the HTML. It's a background picture, named in the stylesheet. So the browser found it only once the stylesheet had arrived, a round trip later. The 36 separate files are named in the HTML, so the browser asked for them straight away.
The table sums up each trick. Read a row from left to right: the trick, why it helped, and what to do now.
| The trick | Why did it help on HTTP/1.1? | What should you do on HTTP/2? |
|---|---|---|
| Domain sharding | Each extra hostname got six more connections, so more requests could go at once. | Drop it. Each extra hostname costs a DNS lookup, a connection and a TLS handshake. It also splits the browser's priorities across connections. |
| Spriting | One request brought dozens of pictures. | Use separate files. With a sprite, every picture waits for the whole file, and changing one picture changes the whole file. |
| Concatenation | Fewer requests for scripts and stylesheets. | Keep it in moderation. One giant bundle downloads again whole after any change, so split it by page and by how often it changes. |
| Inlining | A file inside the HTML needs no request at all. | Keep it small. An inlined file can't be cached on its own, so inline only the small CSS the first screen needs. |
Most sites can drop these habits now. The Web Almanac found that 85% of requests in 2024 went over HTTP/2 or newer. Grigorik's advice for HTTP/2 is to drop domain sharding, and to use concatenation and spriting much less.
Why server push was removed
HTTP/2 had one more idea. A server could push files before the browser asked for them. For example, it could send the stylesheet along with the HTML, and save the browser a round trip.
The trouble was the browser's cache, its store of files it downloaded before. The server couldn't see it. So it often pushed files the browser already had. It's like a friend who brings you milk on every visit, without checking your fridge.
Chrome looked at the results and found no clear gain. Only 1.25% of HTTP/2 sites used push at all. Chrome turned it off in Chrome 106, in 2022. Firefox did the same in Firefox 132, in 2024.
Its replacement is 103 Early Hints. While the server is still preparing the page, it sends a quick early answer, with the status code 103. That answer lists files the browser should start loading, and hosts it should connect to. The browser then fetches only what it doesn't already have. It's like that friend phoning first: "Do you need milk?"
Chrome and Safari accept Early Hints only over HTTP/2 and newer.
Keep it or drop it?
Some habits were workarounds for HTTP/1.1. Others help on any version. Sort each one into a pile:
- Splitting pictures across img1. and img2. hostnames
- Merging 36 small icons into one sprite
- One 2 MB bundle for every page of the site
- Compressing text files with Brotli
- Bundling 400 tiny modules into a few files
- Removing a script no page uses
- Pushing the stylesheet along with the HTML
- Pushing the fonts on every visit
Sort each habit: drop it on HTTP/2, keep it, or use Early Hints or preload instead.
Common mix-ups
- "HTTP/2 makes every page faster." No. It helps most when a page has many requests. A page of a few big files gains little. And on a network that loses packets, one TCP connection can hold up every stream at once.
- "Bundling is pointless on HTTP/2." No. Hundreds of tiny files still cost requests, and they compress worse. Bundle in moderation.
- "More hostnames mean more speed." No, not on HTTP/2. Each new hostname costs a lookup, a connection and a handshake, unless coalescing lets it share one.
- "HTTP/2 ended head-of-line blocking." No, only inside HTTP. TCP still hands bytes over in order, so one lost packet holds up every stream. The next lesson shows how HTTP/3 fixes that.
- "Server push is the fastest way to send CSS." No. Chrome and Firefox removed it. Use 103 Early Hints or a preload instead.
- "Headers are too small to matter." No. As HTTP/1.1 text, the recorded headers were over 600 bytes for every request, and this page set no cookies.
Practice exercises
Try each one before you open the hints.
The tea page grows to 60 pictures. What does this print over HTTP/1.1?
const requests = 61; // the stylesheet and 60 pictures
const atOnce = 6; // six connections, one request on each
const rttMs = 150; // one round trip on Ravi's line
const waves = Math.ceil(requests / atOnce);
console.log(`HTTP/1.1 needs ${waves} waves: about ${waves * rttMs} ms.`);HTTP/1.1 needs 11 waves: about 1650 ms. Ten waves carry 60 requests, so the 61st needs an eleventh. HTTP/2 would still need one wave.
The pictures are now split across two hostnames, so the browser can open twelve connections. What does this print?
const requests = 37; // the stylesheet and 36 pictures
const atOnce = 12; // two hostnames, six connections each
const rttMs = 150; // one round trip on Ravi's line
const waves = Math.ceil(requests / atOnce);
console.log(`HTTP/1.1 needs ${waves} waves: about ${waves * rttMs} ms.`);HTTP/1.1 needs 4 waves: about 600 ms. Sharding did help HTTP/1.1. But each extra hostname also cost a lookup, a connection and a handshake, which this sum leaves out.
The site adds cdn.example.net to its certificate. What does this print now?
const connection = {
ip: "203.0.113.10",
certificate: ["www.example.com", "img.example.com", "cdn.example.net"],
};
const cdn = { host: "cdn.example.net", ip: "203.0.113.10" };
const sameAddress = cdn.ip === connection.ip;
const onCertificate = connection.certificate.includes(cdn.host);
const shares = sameAddress && onCertificate;
console.log(`${cdn.host} ${shares ? "shares the connection" : "needs its own connection"}.`);cdn.example.net shares the connection. The address matches, and now the certificate covers the name, so both checks pass.
In the recorded HTTP/2 load with one sprite, how many round trips after the HTML did the last picture arrive?
2: one round trip for the stylesheet, then one for the sprite. The 36 separate files needed only one, because the HTML names them.
In DevTools' Network panel, which column can you turn on to see whether each request came over HTTP/2?
Protocol. Turn it on, and each request shows the version of HTTP it came over. On HTTP/1.1, time spent waiting for a free connection shows in the Timing tab as Queueing or Stalled.
A page on www.example.com loads its pictures from img1.example.com and img2.example.com. All three names lead to the same IP address, and one certificate covers all three. Over HTTP/2, how many connections can the browser use for all of them?
1. Each picture hostname has the same address, and the certificate covers it. So the browser can send every request on its one connection to www.example.com. Coalescing undoes the sharding.
Check your understanding
For each question, ask yourself: is it about connections, streams, or headers?
Question 1 of 8A smaller page needs 13 requests over HTTP/1.1. What does this print?
Read the code, then predictconst requests = 13; // the stylesheet and 12 pictures const atOnce = 6; // six connections, one request on each const rttMs = 150; // one round trip on Ravi's line const waves = Math.ceil(requests / atOnce); console.log(`HTTP/1.1 needs ${waves} waves: about ${waves * rttMs} ms.`);Choose an answer to see the explanation.
Question 2 of 8Why do browsers open up to six connections to one host over HTTP/1.1?
Choose an answer to see the explanation.
Question 3 of 8In the recorded loads with every request on one shared connection, which brought the last picture in sooner: separate files or the CSS sprite?
Choose an answer to see the explanation.
Question 4 of 8What does the last line of the HTTP/2 headers example print?
Read the code, then predict// The same headers, sent over one HTTP/2 connection. const headerBytes = { first: 407, all: 1616 }; const requests = 36; const eachAfter = Math.round((headerBytes.all - headerBytes.first) / (requests - 1)); console.log(`The first request's headers took ${headerBytes.first} bytes.`); console.log(`Each request after it took about ${eachAfter} bytes.`);Choose an answer to see the explanation.
Question 5 of 8On one shared HTTP/2 connection, who decides which response comes back first?
Choose an answer to see the explanation.
Question 6 of 8When can a browser send requests for img.example.com over an HTTP/2 connection it opened to www.example.com?
Choose an answer to see the explanation.
Question 7 of 8Why did Chrome remove HTTP/2 server push?
Choose an answer to see the explanation.
Question 8 of 8Is domain sharding a good idea on HTTP/2?
Choose an answer to see the explanation.
Key takeaways
- HTTP/1.1 carries one request at a time on each connection. So many files load in waves, six at a time.
- HTTP/2 sends every request at once on one connection, as frames of numbered streams. In the recordings, the pictures took one round trip instead of seven.
- HPACK sends repeated headers as a few bytes instead of hundreds.
- The browser says what matters first. The server or CDN has to follow it.
- A connection can serve another hostname at the same address, if the certificate covers it.
- Drop sharding and sprites, bundle in moderation, and use Early Hints instead of push.
Remember the one-liner.
One connection, many requests: drop the tricks that worked around six.
Sources
The facts on this page come from these standards, books and docs:
- HTTP/1.X, High Performance Browser Networking, Ilya Grigorik.
- Connection management in HTTP/1.x, MDN.
- HTTP/2, High Performance Browser Networking, Ilya Grigorik.
- RFC 9113: HTTP/2, IETF.
- RFC 7541: HPACK, Header Compression for HTTP/2, IETF.
- RFC 9218: Extensible Prioritization Scheme for HTTP, IETF.
- Optimizing Application Delivery, High Performance Browser Networking, Ilya Grigorik.
- HTTP, Web Almanac 2024, HTTP Archive.
- Removing HTTP/2 Server Push from Chrome, Barry Pollard, Chrome for Developers.
- Firefox 132 release notes for developers, MDN.
- 103 Early Hints, MDN.
- PerformanceResourceTiming: nextHopProtocol, MDN.
- NetworkLogViewColumns.ts: the Network panel's columns, Chrome DevTools source.
Coming next: HTTP/3 and QUIC, also in How the browser loads and renders. HTTP/2 still has head-of-line blocking, one layer down, inside TCP. The next lesson shows how QUIC removes it.