completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

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.

By the end, you can
  • 01
    Explain 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.
  • 02
    Explain 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.
  • 03
    Choose 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 picture arrivedPop out in the code editor (opens in a new tab)JavaScript
// 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. and Over 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:

Two timelines. HTTP/1.1: six connections, each carrying one request at a time, so requests 7 to 12 wait for 1 to 6, and the 37th goes alone in a seventh round trip. HTTP/2: one connection carrying all 37 requests in the first round trip. Recorded, HTTP/1.1's pictures came in 7 waves, the last at 1,279 ms; HTTP/2's all at once, the last at 347 ms.round trip 1round trip 2round trip 3round trip 7HTTP/1.1six connections1713…372814…3915…41016…51117…61218…HTTP/2one connection1 to 37all 37 at once: the last picture at 347 ms
Each box is one request and its response. On HTTP/1.1, each of the six connections carries one at a time, so the 37 requests go in seven waves. On HTTP/2, one connection carries all 37 at once.

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:

Real-life analogyOne checkout lane, then six

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.
The main idea

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:

Six at a time, in wavesPop out in the code editor (opens in a new tab)JavaScript
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 real HTTP/1.1 loadPop out in the code editor (opens in a new tab)JavaScript
// 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:

  1. Nothing drawn yet

    0 ms to 347 msA blank screen

  2. The catalogue's green header and its title, with no pictures drawn yet.

    At 347 msThe header and title

  3. The catalogue with tea pictures drawn in its grid; 5 of its 36 pictures had downloaded.

    At 365 ms5 of 36 pictures in

  4. The catalogue with tea pictures drawn in its grid; 11 of its 36 pictures had downloaded.

    At 493 ms11 of 36 pictures in

  5. The catalogue with tea pictures drawn in its grid; 17 of its 36 pictures had downloaded.

    At 654 ms17 of 36 pictures in

  6. The catalogue with tea pictures drawn in its grid; 23 of its 36 pictures had downloaded.

    At 813 ms23 of 36 pictures in

HTTP/1.1: the pictures appear one wave at a time. The screen is full at 813 ms, while the pictures below it are still downloading.

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é:

Real-life analogyOne tray for many tables

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:

A server and a browser joined by one connection. On it, eight frames take turns: 1, 3, 5, 1, 3, 5, 3, 5. Each number is a stream. Stream 1 carries styles.css, stream 3 assam.svg, and stream 5 darjeeling.svg.serverbrowserone connection, frames taking turns13513535each number is a stream: one request and its response1: styles.css3: assam.svg5: darjeeling.svg
Three files share one connection. Their frames take turns, and each frame carries its stream's number. The browser uses the numbers to put 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:

Streams instead of six connectionsPop out in the code editor (opens in a new tab)JavaScript
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 real HTTP/2 loadPop out in the code editor (opens in a new tab)JavaScript
// 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.

  1. Nothing drawn yet

    0 ms to 345 msA blank screen

  2. The catalogue's green header and its title, with no pictures drawn yet.

    At 345 msThe header and title

  3. The catalogue with tea pictures drawn in its grid; 36 of its 36 pictures had downloaded.

    At 362 ms36 of 36 pictures in

HTTP/2: every picture appears at once, about one round trip after the HTML.

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:

Playground: four real loads
The requests0 to 1.4 s
HTTP/1.1, 36 picture filesrecorded
Protocolhttp/1.1
Requests after the page37
Waves of pictures5 + 6 + 6 + 6 + 6 + 6 + 1
Last picture1,279 ms
After the HTML7 round trips
Try it yourself
What happened

HTTP/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.

Real loads of the catalogue in Chrome over HTTPS, on a line like Ravi's: every request had a 150 ms round trip added, at 20 Mbps. Each bar is one request, from when it was sent to when its last byte arrived. The page's own bar runs from the start to the end of its 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.

Real-life analogyHeader compression is writing "same as before"

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:

Headers as HTTP/1.1 textPop out in the code editor (opens in a new tab)JavaScript
// 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. and Each request after it took about 627 bytes.

Every request paid the full price again. Now the same headers over HTTP/2:

The same headers over HTTP/2Pop out in the code editor (opens in a new tab)JavaScript
// 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. and Each 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:

Playground: this page's protocols
What Look does, shortenedJavaScript
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);
This pagenot read yet

Nothing read yet.

Try it yourself
Your page

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.

Read from your own browser with Resource Timing. nextHopProtocol is the protocol each file came over. requestStart is when its request was sent, and responseEnd is when its last byte arrived.

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.

Real-life analogyTelling the kitchen what to bring first

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:

Two ways to say what matters
Which wayHow 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:

Real-life analogyTwo 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:

Can img.example.com share the connection?Pop out in the code editor (opens in a new tab)JavaScript
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:

Two hostnames that can't sharePop out in the code editor (opens in a new tab)JavaScript
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? false and Is api.example.com at the same address? false
Three hostnames on the left. www.example.com and img.example.com resolve to the same address, 203.0.113.10, whose certificate covers both, so their requests share one HTTP/2 connection. api.example.com resolves to 198.51.100.7 and gets a connection of its own.www.example.comimg.example.comapi.example.com203.0.113.10certificate: www. and img.198.51.100.7one connectionits own connection
img.example.com is at the same address, and the certificate lists it. So its requests ride on www.example.com's connection, with no new connection and no new handshake. api.example.com is at another address, so it gets its own.

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:

The sprite on HTTP/1.1Pop out in the code editor (opens in a new tab)JavaScript
// 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:

The sprite on HTTP/2Pop out in the code editor (opens in a new tab)JavaScript
// 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.

HTTP/1.1's tricks on HTTP/2
The trickWhy did it help on HTTP/1.1?What should you do on HTTP/2?
Domain shardingEach 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.
SpritingOne 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.
ConcatenationFewer 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.
InliningA 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:

Keep it or drop it?
  • 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
Try it yourself
0 of 8 correct

Sort each habit: drop it on HTTP/2, keep it, or use Early Hints or preload instead.

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

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.

Exercise 1 · Warm-upMore pictures

The tea page grows to 60 pictures. What does this print over HTTP/1.1?

Starter codePop out in the code editor (opens in a new tab)JavaScript
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.`);

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

    Exercise 2 · Warm-upTwo shards

    The pictures are now split across two hostnames, so the browser can open twelve connections. What does this print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    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.`);

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

      Exercise 3 · PracticeA wider certificate

      The site adds cdn.example.net to its certificate. What does this print now?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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"}.`);

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

        Exercise 4 · PracticeThe sprite's round trips

        In the recorded HTTP/2 load with one sprite, how many round trips after the HTML did the last picture arrive?

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

          Exercise 5 · ChallengeThe protocol in DevTools

          In DevTools' Network panel, which column can you turn on to see whether each request came over HTTP/2?

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

            Exercise 6 · ChallengeShards on HTTP/2

            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?

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

              Check your understanding

              For each question, ask yourself: is it about connections, streams, or headers?

              The HTTP/2 quiz · 8 questionsScore: first tries count
              1. Question 1 of 8A smaller page needs 13 requests over HTTP/1.1. What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const 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.

              2. 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.

              3. 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.

              4. Question 4 of 8What does the last line of the HTTP/2 headers example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // 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.

              5. Question 5 of 8On one shared HTTP/2 connection, who decides which response comes back first?

                Choose an answer to see the explanation.

              6. 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.

              7. Question 7 of 8Why did Chrome remove HTTP/2 server push?

                Choose an answer to see the explanation.

              8. 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:

              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.

              CompleteFrontend Clear concepts. Working examples.