103 Early Hints
Let the browser start fetching critical resources while the server is still building the page. A shop that thinks for 400 ms, recorded in Chrome over a real round trip: no hints, a preconnect, preloads, a stale hint, files that can't be cached, HTTP/1.1, a repeat visit and a flushed head, and what each does to TTFB, FCP and LCP.
- 01Explain a 103Say what the server sends while it thinks, and what the browser does with it before the page arrives.
- 02Choose what to hintPreload files every visit needs, preconnect to servers when you don't know the file, and skip the rest.
- 03Measure it honestlyTTFB counts the 103, so judge the gain by FCP, LCP and when the 200 really arrived.
Using the server's think time
Asha taps a link to the Mint tea shop. Her browser sends the request. Then nothing happens for a while. The shop's server needs 400 ms to build her page. All that time, the connection between them sits silent.
Chrome's guide to Early Hints calls this wait server think-time. In its words, "the connection effectively goes idle for as long as it takes the server to prepare the response". Idle means doing nothing.
Yet the browser could be busy. Every page on the shop needs the same stylesheet and the same big photo. A 103 Early Hints response is a short message the server sends before the page. It tells the browser the names of those files. Then the browser can fetch them while the server thinks. Faster server responses filled this wait by sending the top of the page early. A 103 fills it without sending any of the page. So how much did it help Asha?
How the course timed it
The course built the shop and timed it in Chrome:
- The shop lives at
shop.test. Its page is small: a heading, one big photo, and a link. Its stylesheet,/main.css, lives on the shop too. - The big photo near the top is the hero. It lives on an image CDN at
cdn.test. A CDN, a content delivery network, is a service that hosts files like images. Here it's a second server. So the browser needs a second connection. - Both servers spoke HTTP/2, over HTTPS.
- A delay proxy sat in between. It's a small go-between program. It held every message for 50 ms on its way. So each round trip took 100 ms. A round trip is a message there and its answer back. A new connection cost one more round trip, as a TCP handshake would.
- Chrome loaded each version 5 times. Each time, it started with no open connection and an empty cache. The lesson quotes the middle value, the median.
Here is what the course found, without hints and with them:
// Asha's visit to the Mint tea shop. Medians of 5 runs, in ms from her click.const noHints = { pageArrived: 708.5, heroPainted: 1032 };const withHints = { pageArrived: 706.4, heroPainted: 748 }; console.log(`Without hints, the page arrived at ${noHints.pageArrived} ms and the hero was painted at ${noHints.heroPainted} ms.`);console.log(`With hints, the page arrived at ${withHints.pageArrived} ms and the hero was painted at ${withHints.heroPainted} ms.`);- Line 1 says the times are in milliseconds (ms). They count from Asha's click.
- Line 2 holds two times without hints.
pageArrivedis when the page's own response started to arrive.heroPaintedis when the hero appeared on the screen. - Line 3 holds the same two times when the shop sent a 103 first.
- Line 5 prints
Without hints, the page arrived at 708.5 ms and the hero was painted at 1032 ms. - Line 6 prints
With hints, the page arrived at 706.4 ms and the hero was painted at 748 ms.
The page arrived at almost the same moment both times. Yet with hints, the hero appeared 284 ms sooner. How can a page that arrives no sooner be painted sooner? Here is the whole visit at once:
Without hints, the browser learned the files' names only when the page arrived. With hints, it learned them at 304.6 ms, from the 103. That was about three round trips after the click. One opened the connection. One set up TLS, the encryption that makes the line private. The third carried the request and brought back the 103. So the browser fetched both files while the shop was still thinking.
Table 4 orders a curry, and the kitchen needs twenty minutes for it. A good waiter doesn't stand at the kitchen door until it's ready. Passing the bar, they say: "Table 4 will want water and bread." The curry lands. The water is poured and the bread is warm.
- In real life: The kitchen cooking the curry
- In a page load: The server's think time, building the page
- In real life: The waiter's word to the bar
- In a page load: The
103, withLinkheaders that name files - In real life: The bar pouring water and warming bread
- In a page load: The browser fetching
main.cssand the hero - In real life: The curry landing on a table that's ready
- In a page load: The
200arriving, with its files already fetched
A 103 Early Hints response is a short message a server sends while it's still building the page. It names files and servers the page will need. The browser starts on them at once. It helps only when the server needs time to think, and only with files the page will surely use.
The course recorded seven versions of the shop. Pick one below. Start with Preload CSS and hero, then try No hints:
- The 103 arrived304.6 ms
- The 200 arrived706.4 ms
- First contentful paint748 ms
- Largest contentful paint748 ms
- main.css asked for106.7 ms
- hero.jpg asked for311.3 ms
Medians of 5 runs. Each round trip took 100 ms, and the shop thought for 400 ms before it sent the page. The first four rows count from Asha's click. The other rows count from when the shop got her request: each one is when a server was asked for a file.
How 103 Early Hints works
Every HTTP response starts with a three-digit status code. For example, 200 means OK: here is your page. But a server may send short messages first. It sends them while it works on the real answer.
A response before the response
Those early messages use the 1xx status codes, from 100 to 199. RFC 9110 is the standard for HTTP. It calls them "an interim response": one for the meantime. It comes before the final response. The final response is the real answer, like the 200 with the page.
An interim response is only headers: the labelled lines that come before a response's content. RFC 9110 says it "cannot contain content or trailers." So a 103 can't carry any of the page.
RFC 8297 defines 103 Early Hints. Here, the client is the browser. A 103 "indicates to the client that the server is likely to send a final response with the header fields included in the informational response". In plain words: "here are some headers my real answer will probably have."
The hints are Link headers
The header that matters is Link. Each value names a file or a server. Its rel part says what to do with it. A browser acts on two kinds:
- preload means: fetch this file now. The shop's 103 had two:
</main.css>; rel=preload; as=styleand<https://cdn.test/hero.jpg>; rel=preload; as=image. The address goes between<and>. Theaspart says what kind of file it is. - preconnect means: open a connection to this server now.
<https://cdn.test>; rel=preconnectnames a server, not a file. The browser opens the connection, handshakes and all. Then it's ready when a file from that server is needed.
Chrome uses only these two. Its guide says Chrome "Only supports preconnect and preload (that is, prefetch isn't supported)".
You met preload as an HTML tag in HTML parsing and the preload scanner. There, a helper skimmed the recipe for ingredients. During the think time, there's no recipe to skim yet. A 103 hands the helper a shopping list before the recipe comes. As Chrome's guide puts it, "For Early Hints, you won't have the HTML".
Two arrivals in one visit
The browser writes down both responses in the page's navigation entry. That's the record of timings from The navigation timeline. Two fields matter here:
firstInterimResponseStartis when the first interim response, the 103, started to arrive.finalResponseHeadersStartis when the final response's headers, the 200's, started to arrive.
// Asha's visit with a 103, from the page's navigation entry. Medians of 5 runs, in ms from her click.const entry = { firstInterimResponseStart: 304.6, finalResponseHeadersStart: 706.4 }; console.log(`The 103 arrived at ${entry.firstInterimResponseStart} ms.`);console.log(`The 200 arrived at ${entry.finalResponseHeadersStart} ms.`);console.log(`The browser had ${(entry.finalResponseHeadersStart - entry.firstInterimResponseStart).toFixed(1)} ms in between.`);- Line 2 holds the two recorded times, from the visits where the shop sent a 103.
- Lines 4 and 5 print
The 103 arrived at 304.6 ms.andThe 200 arrived at 706.4 ms. - Line 6 subtracts one from the other. It prints
The browser had 401.8 ms in between.
The 103 left the shop the moment Asha's request arrived. The 200 left when the shop had finished thinking. So the gap is the think time, handed to the browser. The longer your server thinks, the more a 103 can fill.
The browser's rules
Chrome's guide and the HTML standard list the rules browsers follow:
- Navigations only. Early Hints are "Only available for navigation requests". Those are requests for the page itself, not for its files.
- The first 103 only. The HTML standard: "Only the first early hint response served during the navigation is handled".
- Not after a redirect to another site. An origin is the scheme, host and port of an address. If the final response redirects to another origin, the hints are "discarded". Redirects covers redirects.
- Through the HTTP cache. Preloaded files "are stored in the HTTP cache and retrieved from there by the page later". The next section shows why that matters.
- Hints, not orders. RFC 8297: they "do not replace the header fields on the final response". The page must still link its own stylesheet and hero.
What to hint
First, check that there's a wait to fill. Chrome's guide warns: "Early Hints are not useful if your server can send a 200 (or other final responses) right away". If yours can, use the usual preload or preconnect instead. Put them in the 200's Link header or in the HTML.
At a sandwich counter, your sandwich is made while you watch. A waiter who ran ahead to the bar would get back after the sandwich did. So nobody runs. You just ask for water with your order.
- In real life: A sandwich made while you watch
- In a page load: A server that sends its
200right away - In real life: A waiter running ahead to the bar
- In a page load: A
103sent first - In real life: Asking for water with your order
- In a page load:
preloadorpreconnectin the200'sLinkheader, or in the HTML
The shop thinks for 400 ms, so it has a wait to fill. Which files, then? Chrome's guide suggests two steps:
- Start with your landing pages, where visits begin. Chrome says "the usefulness of Early Hints decreases as the user navigates around your website". That's because more of the files are already cached.
- Hint what the first paint waits for. Chrome names "render-blocking subresources such as synchronous JavaScript, stylesheets, or even web fonts". Hint the servers that host them too. A render-blocking file must arrive before anything is painted. You saw this in Render-blocking and parser-blocking resources.
Preload: the files every visit needs
The shop's page needs two files before it looks right: the stylesheet and the hero. They're the same on every visit. So the course preloaded both. This time the clock is the servers'. It shows when they were asked for each file, counted from when the shop got Asha's request.
// When the servers were asked for each file, in ms after the shop got Asha's request. Medians of 5 runs.const noHints = { stylesheet: 511.3, hero: 716.2 };const preloads = { stylesheet: 106.7, hero: 311.3 }; console.log(`Without hints: the stylesheet at ${noHints.stylesheet} ms, the hero at ${noHints.hero} ms.`);console.log(`With preloads: the stylesheet at ${preloads.stylesheet} ms, the hero at ${preloads.hero} ms.`);- Line 2 holds the times without hints. Line 3 holds them with preloads.
- Line 5 prints
Without hints: the stylesheet at 511.3 ms, the hero at 716.2 ms. - Line 6 prints
With preloads: the stylesheet at 106.7 ms, the hero at 311.3 ms.
Without hints, nothing was asked for until the page reached Chrome. That came after the shop's 400 ms of thinking. The hero came about two round trips after the stylesheet. The CDN needed a new connection first. You saw its handshakes in TCP connections and slow start and TLS without the wait.
With preloads, the stylesheet was asked for about one round trip in. That's the 103 travelling to Chrome, and the request coming back. The hero again came two round trips later. Both were asked for before the shop finished thinking. Why it matters: when the page arrived, both files were already coming.
Preconnect: the server, when you don't know the file
Sometimes you know the server a file comes from, but not the file. Say the hero depends on what the server finds in its database. Chrome wants hints "largely independent of the outcome for the main resource". So you can't preload that hero. But you can connect to its server. It's like telling the bar "table 4 will want drinks" before they've chosen which. The course sent a 103 with only <https://cdn.test>; rel=preconnect:
// The same clock, with a 103 that only said: connect to the CDN. Medians of 5 runs.const preconnect = { stylesheet: 515.7, hero: 514.3 }; console.log(`With a preconnect: the stylesheet at ${preconnect.stylesheet} ms, the hero at ${preconnect.hero} ms.`);- Line 2 holds the two recorded times, on the same clock as before.
- Line 4 prints
With a preconnect: the stylesheet at 515.7 ms, the hero at 514.3 ms.
The stylesheet came no sooner. Chrome still had to read the page to find it. But the hero was asked for together with it, on a connection that was already open. That saved 201.9 ms on the hero. Here are all three versions, file by file:
What Asha saw
Asking sooner only matters if the page appears sooner. First Contentful Paint (FCP) is when the first text or image appears. Largest Contentful Paint (LCP) is when the largest one does: here, the hero.
// What Asha saw, in ms from her click. Medians of 5 runs.const visits = { "No hints": { fcp: 828, lcp: 1032 }, "A preconnect": { fcp: 848, lcp: 848 }, "Preloads": { fcp: 748, lcp: 748 },}; for (const [hint, paint] of Object.entries(visits)) console.log(`${hint}: first paint at ${paint.fcp} ms, hero at ${paint.lcp} ms`);- Lines 3 to 5 hold each version's two paint times.
- Line 8 prints one line for each version. First
No hints: first paint at 828 ms, hero at 1032 ms. - Then
A preconnect: first paint at 848 ms, hero at 848 msandPreloads: first paint at 748 ms, hero at 748 ms.
Preloads did best. The text and the hero appeared together, 284 ms before the hero did with no hints. The preconnect helped only the hero. The stylesheet was still asked for after the page arrived. Why it matters: preload the files you know, and preconnect when you only know the server.
Hints that cost more than they save
A hint makes the browser spend data before it has seen the page. If the guess is wrong, that work is wasted.
The waiter tells the bar: "Table 4 will want two lemonades." But table 4 has changed its mind. It wants tea. The bar pours the lemonades anyway, and nobody drinks them. The tea is only made when the real order comes. It's as if the waiter had said nothing.
- In real life: Two lemonades nobody drinks
- In a page load: A hinted file the page doesn't use
- In real life: The lemons and time spent pouring them
- In a page load: The data spent downloading that file
- In real life: The tea, made only when the real order comes
- In a page load: The right file, fetched only once the page names it
The course tried three ways. The first is a stale hint, one that's out of date. A site can put a version or a hash in a file's name, like main.fa231e9c.css. A hash is a short code worked out from the file's content. Then the name changes with every version. Chrome warns such files "may not be the best choice". The name in the 103 can fall behind the name in the page. The course's 103 named main.v1.css. That's a stylesheet the page no longer links:
// A stale hint: the 103 named main.v1.css, but the page now links main.css. The same in all 5 runs.const sent = ["main.v1.css", "main.css", "hero.jpg"];const namedInThePage = ["main.css", "hero.jpg"]; const wasted = sent.filter((file) => !namedInThePage.includes(file));console.log(`The servers sent ${sent.length} files.`);console.log(`Sent for nothing: ${wasted.join(", ")}.`);- Line 2 lists the files the servers sent, in order. The list was the same in all 5 runs.
- Line 3 lists the files the page itself names.
- Line 5 keeps the files that were sent but aren't named in the page.
- Lines 6 and 7 print
The servers sent 3 files.andSent for nothing: main.v1.css.
The hero was painted at 1,032 ms, exactly as with no hints. The wrong file cost a download and saved nothing. Chrome says this is "best dealt with automation or templating". So let your build write the hints. Then the names can't drift apart.
The second is a file that can't be cached. A preloaded file waits in the HTTP cache until the page asks for it. So "only cacheable resources can be preloaded using Early Hints or the resource will be double fetched". The course sent both files with Cache-Control: no-store. RFC 9111 says that means a cache "MUST NOT store any part" of the response. Here is what happened:
// Preloads of files sent with Cache-Control: no-store. The files the servers sent, the same in all 5 runs.const sent = ["main.css", "hero.jpg", "main.css", "hero.jpg"]; const timesSent = (file) => sent.filter((name) => name === file).length;console.log(`main.css was sent ${timesSent("main.css")} times.`);console.log(`hero.jpg was sent ${timesSent("hero.jpg")} times.`);- Line 2 lists the files the servers sent, in order. The list was the same in all 5 runs.
- Line 4 counts how many times one file appears in that list.
- Lines 5 and 6 print
main.css was sent 2 times.andhero.jpg was sent 2 times.
Each file came twice: once for the hint, and again for the page. Even so, the hero was painted at 852 ms, not 1,032. The wasted fetch had opened the connection to the CDN, like a preconnect. Why it matters: every double download costs your visitors data.
The third is a repeat visit. Asha opened a second page on the shop. Both files were already in her cache:
// Asha's second page on the shop, with both files already in her browser's cache. Medians of 5 runs.const noHints = 744;const withHints = 744; console.log(`Without hints, the hero was painted at ${noHints} ms.`);console.log(`With hints, it was painted at ${withHints} ms.`);- Lines 2 and 3 hold when the hero was painted, without and with hints.
- Lines 5 and 6 print
Without hints, the hero was painted at 744 ms.andWith hints, it was painted at 744 ms.
The servers sent no files at all. Both came from the cache, so the hints had nothing to speed up. That's why Chrome starts with landing pages. Here are all six versions side by side:
Browser support for preconnect and preload hints
Each browser decides what it will act on. Chrome's guide: "Although 103 Early Hints is supported in all major browsers, the directives that can be sent on an Early Hint differs per browser". A directive here is the rel value: preconnect or preload.
The kitchen pins a note on its back door for the morning deliveries. It says: "Extra bread and extra milk today." The baker reads it and brings more bread. The milkman reads only his own line. The fruit seller never looks at the door. He brings his usual order at his usual time.
- In real life: The note on the door
- In a page load: The
103and itsLinkhints - In real life: The baker, who acts on the whole note
- In a page load: Chrome and Firefox:
preconnectandpreload - In real life: The milkman, who acts on his own line only
- In a page load: Safari:
preconnectonly - In real life: The fruit seller, who walks past
- In a page load: A browser that ignores the 103, like Chrome over HTTP/1.1
MDN's compatibility data says who acts on what:
preconnectworks in Chrome 103, Firefox 120 and Safari 17, and in later versions.preloadworks in Chrome 103 and Firefox 123, and in later versions. Safari doesn't preload from a 103.
So on Safari, a preload in a 103 does nothing. But a preconnect to the same server still works. Why it matters: when a key file lives on another server, a preconnect to that server reaches more browsers.
Only over HTTP/2 and later
The protocol matters too. MDN's data notes that Chrome and Safari support a 103 "in HTTP/2 and later only". RFC 8297 explains the risk. An old HTTP/1.1 client could mistake the 103 for the real answer. Then it would mix up every response after it on that connection. So "a server might refrain from sending 103 (Early Hints) responses over HTTP/1.1 unless the client is known to handle informational responses correctly". To refrain is to hold back.
The course sent the same 103 to a Chrome kept to HTTP/1.1:
// The same 103 with preloads, to Chrome over HTTP/2 and to a Chrome kept to HTTP/1.1. Medians of 5 runs, in ms.const http2 = { hintsArrived: 304.6, heroPainted: 748 };const http1 = { hintsArrived: 306.1, heroPainted: 1032 }; console.log(`Over HTTP/2, the 103 arrived at ${http2.hintsArrived} ms. The hero was painted at ${http2.heroPainted} ms.`);console.log(`Over HTTP/1.1, the 103 arrived at ${http1.hintsArrived} ms. The hero was painted at ${http1.heroPainted} ms.`);- Lines 2 and 3 hold one protocol each. They say when the 103 arrived and when the hero was painted.
- Line 5 prints
Over HTTP/2, the 103 arrived at 304.6 ms. The hero was painted at 748 ms. - Line 6 prints
Over HTTP/1.1, the 103 arrived at 306.1 ms. The hero was painted at 1032 ms.
Over HTTP/1.1, Chrome wrote down when the 103 arrived. Then it ignored its hints. On the shop's clock, the stylesheet was asked for only at 510.4 ms, after the page. That's the fruit seller walking past the note. Why it matters: over HTTP/1.1, a 103 won't help visitors on Chrome or Safari.
CDN and server support
Now the sending side. Which servers can send a 103? Chrome's guide lists these open-source web servers:
- Apache, with its
mod_http2module. - H2O.
- NGINX.
- Node.js, in both its
httpandhttp2modules.
It also lists CDNs that offer Early Hints: Cloudflare, Fastly and Akamai. With one in front of your server, "you may not need to manually implement Early Hints". To learn what yours does, Chrome says to "Refer to your solution provider's online documentation".
The shop's server
The course's shop is a Node.js server. Node's response.writeEarlyHints() "Sends a status 103 Early Hints to the client with a Link header". Here is the shop's function that answers a page request:
async function shopPage(request, response, hints) { if (request.headers["sec-fetch-mode"] === "navigate") response.writeEarlyHints({ link: hints }); const html = await buildPage(); response.writeHead(200, { "content-type": "text/html", link: hints }).end(html);}- Line 1 starts the function.
hintsis the list of Link values, like</main.css>; rel=preload; as=style. - Line 2 sends the 103 with those hints, but only if the request is a navigation.
- Line 3 builds the page. This is the 400 ms of think time.
- Line 4 sends the 200 with the page. It repeats the hints in its own
Linkheader.
How does line 2 spot a navigation? The browser sends a Sec-Fetch-Mode header. It "exposes a request's mode to a server". The mode says what kind of request it is. The mode navigate is "used only when navigating between documents". Chrome's guide gives two reasons to check it:
- Browsers use Early Hints only for navigations. So a 103 on any other request is wasted.
- "Some older clients or bots may struggle with these". A bot is a program that visits pages on its own. Clients that send
sec-fetch-mode: navigateare newer ones. They "understand to wait for the subsequent response".
Line 4 follows Chrome's advice too. It says: "For backward compatibility, it's good practice to also include Link HTTP headers in the final response". Backward compatibility means still working with older browsers. So a browser that ignored the 103 still gets the hints. Chrome also says Early Hints "are recommended to only be sent over HTTP/2 or HTTP/3 connections". The HTTP/1.1 recording showed why.
Not HTTP/2 Push again
HTTP/2 has a feature called server push. Chrome's guide calls it deprecated: no longer recommended, and on its way out. With push, the server sends files along with the page, without being asked. You saw it in HTTP/1.1 and HTTP/2. It's like a bar sending drinks to every table, even tables that already have some. Chrome: "Chrome data showed that HTTP2/Push was in fact a net negative for performance across the web". A net negative means it made pages slower overall.
A 103 sends only names. So the browser skips what's already in its cache, as on Asha's second page. RFC 8297 names the price: "one extra round trip compared to server push in the worst case". On the shop, that's the round trip before the stylesheet was asked for, at 106.7 ms.
Measuring the gain
The shop added a 103, and the hero appeared 284 ms sooner. How would you see that in your own numbers? Not in TTFB, it turns out.
TTFB counts the 103
Time to first byte (TTFB) is the wait from the start of a visit to the response's first byte. You met it in Time to first byte. With a 103, which response counts? web.dev: "The 103 Early Hints counts as the 'first bytes'."
Your friend times table 4's curry. They stop the stopwatch at the first sign of it. After a minute, the waiter tells the bar what table 4 will want. Click: "One minute, great service!" But the curry took as long as ever.
- In real life: The stopwatch stopping at the waiter's word to the bar
- In a page load: TTFB:
responseStart, the 103's time - In real life: The curry landing
- In a page load: The
200:finalResponseHeadersStart - In real life: The meal you came for
- In a page load: LCP: the hero on the screen
In Time to first byte, the stopwatch stopped at the bread. Here it stops even earlier. TTFB is read from responseStart. The Resource Timing standard says responseStart is "firstInterimResponseStart if it is not 0; Otherwise this's finalResponseHeadersStart". So with a 103, it's the 103's time.
Chrome 115 tried measuring the final response instead. But web.dev says that change "was reverted in Chrome 133". Reverted means undone. So TTFB moves with the 103:
// Medians of 5 runs, in ms from Asha's click: TTFB, when the 200's headers arrived, and LCP.const noHints = { ttfb: 708.5, finalHeaders: 708.5, lcp: 1032 };const withHints = { ttfb: 304.6, finalHeaders: 706.4, lcp: 748 }; console.log(`TTFB came ${(noHints.ttfb - withHints.ttfb).toFixed(1)} ms sooner.`);console.log(`The 200 came ${(noHints.finalHeaders - withHints.finalHeaders).toFixed(1)} ms sooner.`);console.log(`LCP came ${noHints.lcp - withHints.lcp} ms sooner.`);- Line 1 names the three times.
finalHeadersisfinalResponseHeadersStart, when the 200 arrived. - Line 2 holds them without hints. Line 3 holds them with the 103.
- Line 5 prints
TTFB came 403.9 ms sooner. - Line 6 prints
The 200 came 2.1 ms sooner. - Line 7 prints
LCP came 284 ms sooner.
TTFB says the page got about 400 ms faster. It didn't. The 200 came at the same moment. The real gain was 284 ms of LCP. TTFB can even fall with no gain at all. Over HTTP/1.1, Chrome ignored the hints. Yet TTFB fell just as far, to 306.1 ms.
So judge a 103 by three things:
- FCP and LCP: the paints Asha sees.
finalResponseHeadersStart: when the 200 really arrived.- Which files came from the hints.
Which files came from the hints?
Resource Timing keeps a record, an entry, for each file a page loads. Each entry has an initiatorType that says what asked for the file. It's "early-hints" when the request "is the result of processing an Early Hints response". This code lists those files on any page:
const hinted = performance
.getEntriesByType("resource")
.filter((entry) => entry.initiatorType === "early-hints")
.map((entry) => entry.name);
console.log(hinted);- Line 1 starts a list called
hinted, built by lines 2 to 4. - Line 2 gets the Resource Timing entry of every file the page loaded.
- Line 3 keeps the ones whose
initiatorTypeis"early-hints". - Line 4 keeps just their addresses. Line 6 logs the list.
On every visit with preloads, both of the shop's files were marked "early-hints". Their transferSize was 0. The spec reports 0 bytes for a file from the local cache. On the stale-hint visits, main.v1.css was marked too. So the list also shows hints the page never used.
A 103 or a flushed head?
Faster server responses used the think time another way. It flushed the head. To flush is to send what's ready so far, before the rest is done. The head, the top of the page, held the stylesheet and the hero's preload. The course tried that here too:
// Two ways to use the think time. Medians of 5 runs, in ms from Asha's click.const hints = { firstByte: 304.6, heroPainted: 748 };const flushedHead = { firstByte: 306.7, heroPainted: 736 }; console.log(`A 103: first byte at ${hints.firstByte} ms, hero painted at ${hints.heroPainted} ms.`);console.log(`A flushed head: first byte at ${flushedHead.firstByte} ms, hero painted at ${flushedHead.heroPainted} ms.`);- Lines 2 and 3 hold one way each. They say when the first byte came and when the hero was painted.
- Line 5 prints
A 103: first byte at 304.6 ms, hero painted at 748 ms. - Line 6 prints
A flushed head: first byte at 306.7 ms, hero painted at 736 ms.
The two did about as well. web.dev calls early flushing "similar in effect to Early Hints." So why have both? Because a flush sends the 200 before the work is done. A 103 leaves the final answer open. RFC 8297 notes that "the status code might depend on the result of a database query".
Say Asha asks for a tea the shop no longer sells. After a flush, the shop has already said 200. After a 103, it can still answer 404: not found. A flush is the starter already on the table. A 103 is only the waiter's word to the bar. Each row below asks a question. Compare the two answers:
| The question | A 103 | A flushed head |
|---|---|---|
| What leaves the server at once? | Only headers: the Link hints. No part of the page. | The 200, its headers, and the page's <head>. |
| Can the server still choose the status code? | Yes. The final response hasn't started. | No. The 200 has already left. |
| What does TTFB show? | When the 103 arrived: 304.6 ms. | When the 200 and the head arrived: 306.7 ms. |
| When was the hero painted? | At 748 ms. | At 736 ms. |
Hint it or not?
For each candidate, ask: will the page surely use it? Can it be cached? Do you know the file, or only its server? Then sort it:
- /main.css, the same stylesheet on every page
- The landing page's hero image, cached for an hour
- The image CDN, when which hero to show depends on what the server finds
- The image CDN, for visitors on Safari
- A stylesheet sent with Cache-Control: no-store
- A hashed file name, typed into the server's settings by hand
- A page the server can send in a few milliseconds
Sort each candidate by what to do with it in a 103.
Common mix-ups
- "A lower TTFB proves Early Hints worked." No. TTFB stops at the 103. Over HTTP/1.1, it fell to 306.1 ms while LCP stayed at 1,032 ms.
- "A 103 makes the page arrive sooner." No. The 200 came at the same moment. Only its files came sooner.
- "Hints are free." No. A stale hint cost a download, and files that couldn't be cached came twice.
- "You can hint any file, any way." No. Only files that can be cached, with preload or preconnect.
- "Early Hints is HTTP/2 Push again." No. Push sent files. A 103 sends only names.
- "Every browser preloads from a 103." No. Safari acts only on preconnect.
Practice exercises
Answer first, then use the hints.
This 103 named an old stylesheet instead of the real files. What does the last line print?
// The same clock, with a 103 that named main.v1.css, a stylesheet the page no longer links. Medians of 5 runs.
const noHints = { stylesheet: 511.3, hero: 716.2 };
const staleHint = { stylesheet: 510.3, hero: 712.8 };
console.log(`Without hints: the stylesheet at ${noHints.stylesheet} ms, the hero at ${noHints.hero} ms.`);
console.log(`With a stale hint: the stylesheet at ${staleHint.stylesheet} ms, the hero at ${staleHint.hero} ms.`);With a stale hint: the stylesheet at 510.3 ms, the hero at 712.8 ms. That's almost the no-hints line. The 103 fetched a file the page never used, so the real files were asked for only after the page arrived.
The same 103 went to a Chrome kept to HTTP/1.1. What does the first line print?
// The same 103, but Chrome spoke HTTP/1.1. Medians of 5 runs, in ms from Asha's click.
const noHints = { ttfb: 708.5, finalHeaders: 708.5, lcp: 1032 };
const withHints = { ttfb: 306.1, finalHeaders: 706.4, lcp: 1032 };
console.log(`TTFB came ${(noHints.ttfb - withHints.ttfb).toFixed(1)} ms sooner.`);
console.log(`The 200 came ${(noHints.finalHeaders - withHints.finalHeaders).toFixed(1)} ms sooner.`);
console.log(`LCP came ${noHints.lcp - withHints.lcp} ms sooner.`);TTFB came 402.4 ms sooner. Yet the last line prints LCP came 0 ms sooner. TTFB stopped at the 103, even though Chrome didn't use its hints.
With a preconnect to the CDN, when did the 200 arrive, in ms from the click?
708.1 ms: about the same as with no hints, 708.5 ms. A preconnect opens a connection early, but the shop thinks just as long.
When the files couldn't be cached, when was main.css asked for the second time?
508.3 ms after the shop got the request. That's after the page arrived and named the stylesheet. The copy from the hint couldn't be kept, so the page fetched it again.
You test the shop with DevTools open, and the page doesn't use the files the 103 preloaded. Chrome's Early Hints guide names a checkbox in the Network panel that must not be ticked. Which one?
Disable cache. Chrome's guide: 'Disable cache must not be ticked in DevTools as Early Hints uses the browser cache.' With it ticked, the page can't pick up the hinted files.
The shop's hero picks its size for the screen, with imagesrcset and imagesizes on its preload. Can a 103 preload it before the page arrives? Answer yes or no.
No. Chrome's guide says preloading responsive images 'may not be supported using HTTP <link> headers as the viewport is not defined until the document is created.' The HTML standard handles only as, crossorigin, integrity and type in an early hint. imagesrcset and imagesizes wait for the document.
Check your understanding
For each question, ask three things. Did the server have time to spare? Could the browser use the hint? Which number shows the real gain?
Question 1 of 7What does the second line of the measuring example print?
Read the code, then predict// Medians of 5 runs, in ms from Asha's click: TTFB, when the 200's headers arrived, and LCP. const noHints = { ttfb: 708.5, finalHeaders: 708.5, lcp: 1032 }; const withHints = { ttfb: 304.6, finalHeaders: 706.4, lcp: 748 }; console.log(`TTFB came ${(noHints.ttfb - withHints.ttfb).toFixed(1)} ms sooner.`); console.log(`The 200 came ${(noHints.finalHeaders - withHints.finalHeaders).toFixed(1)} ms sooner.`); console.log(`LCP came ${noHints.lcp - withHints.lcp} ms sooner.`);Choose an answer to see the explanation.
Question 2 of 7What does the last line of the stale-hint example print?
Read the code, then predict// A stale hint: the 103 named main.v1.css, but the page now links main.css. The same in all 5 runs. const sent = ["main.v1.css", "main.css", "hero.jpg"]; const namedInThePage = ["main.css", "hero.jpg"]; const wasted = sent.filter((file) => !namedInThePage.includes(file)); console.log(`The servers sent ${sent.length} files.`); console.log(`Sent for nothing: ${wasted.join(", ")}.`);Choose an answer to see the explanation.
Question 3 of 7A server can send its whole page 20 ms after the request arrives. Should it send a 103 first?
Choose an answer to see the explanation.
Question 4 of 7Chrome loads a page over HTTP/1.1 and gets a 103 with preload hints. What happens?
Choose an answer to see the explanation.
Question 5 of 7Why were hinted files sent with Cache-Control: no-store downloaded twice?
Choose an answer to see the explanation.
Question 6 of 7The hero image's name depends on what the server finds in its database. What can the 103 still do for it?
Choose an answer to see the explanation.
Question 7 of 7After you add a 103, field TTFB falls by 400 ms. What should you check next?
Choose an answer to see the explanation.
Key takeaways
- A 103 comes before the page. It names files and servers, so the browser starts on them while the server thinks.
- It only helps when the server needs time. A page sent at once needs no 103.
- Preload files every visit needs, with stable names, that can be cached. Preconnect when you only know the server.
- Send it only for navigations, over HTTP/2 or HTTP/3, and repeat the Link headers in the 200.
- Safari acts only on preconnect. Chrome and Safari ignore a 103 over HTTP/1.1.
- TTFB counts the 103. Judge the gain by FCP, LCP and finalResponseHeadersStart.
Remember the one-liner.
Hint what the page surely needs while the server thinks, and measure the paint, not the first byte.
Sources
The facts on this page come from these standards and docs:
- Faster page loads using server think-time with Early Hints, Chrome for Developers.
- RFC 9110: HTTP Semantics, Informational 1xx, the IETF.
- RFC 8297: An HTTP Status Code for Indicating Hints, the IETF.
- HTML Standard: Early hints, WHATWG.
- RFC 9111: HTTP Caching, the no-store response directive, the IETF.
- 103 Early Hints, MDN.
- Fetch Metadata Request Headers: Sec-Fetch-Mode, W3C.
- Fetch Standard: request mode, WHATWG.
- HTTP/2: response.writeEarlyHints(hints), Node.js.
- Time to First Byte (TTFB), web.dev.
- Resource Timing, W3C.
Coming next: HTTP caching in depth, also in Fast servers, caches, and networks. It covers Cache-Control, freshness and revalidation: how a browser decides to reuse a response. That's the cache the shop's hinted files waited in.