<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VignesH KumaR</title>
    <description>The latest articles on DEV Community by VignesH KumaR (@happyvig).</description>
    <link>https://dev.to/happyvig</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F874238%2F48333dcc-b37a-4efa-a36f-12896de33d7b.jpeg</url>
      <title>DEV Community: VignesH KumaR</title>
      <link>https://dev.to/happyvig</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/happyvig"/>
    <language>en</language>
    <item>
      <title>HTTP 103 Early Hints, Explained the Way I Wish Someone Had Explained It to Me</title>
      <dc:creator>VignesH KumaR</dc:creator>
      <pubDate>Thu, 09 Jul 2026 06:03:57 +0000</pubDate>
      <link>https://dev.to/happyvig/http-103-early-hints-explained-the-way-i-wish-someone-had-explained-it-to-me-3i58</link>
      <guid>https://dev.to/happyvig/http-103-early-hints-explained-the-way-i-wish-someone-had-explained-it-to-me-3i58</guid>
      <description>&lt;h2&gt;
  
  
  🔍 How I got here
&lt;/h2&gt;

&lt;p&gt;I came across &lt;strong&gt;HTTP 103 Early Hints&lt;/strong&gt; while reading about how larger sites shave milliseconds off their load times. The idea is elegant enough to stop and think about: a server usually knows which stylesheet, script, or hero image a page will need well before it finishes the slow part of the request — a database call, a template render, an API round-trip. Early Hints lets the server say so early, via an informational &lt;code&gt;103&lt;/code&gt; response, so the browser can start fetching those resources while the server is still working, instead of waiting for the final &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most of what I read stopped at that explanation. Reasonable diagrams, a sentence about "improves FCP/LCP," and a move on to the next topic. I wanted to actually see it happen — the real bytes on the wire, a real browser reacting to them, and a real before/after comparison — rather than take the concept on faith. So I built a small demo app to study it properly, and this post is what came out of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basics, quickly
&lt;/h2&gt;

&lt;p&gt;Before going further, a handful of load-bearing facts worth having straight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Practical takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Status class&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;103&lt;/code&gt; is informational (&lt;code&gt;1xx&lt;/code&gt;), same family as &lt;code&gt;100 Continue&lt;/code&gt; — it's not the final response, and it's optional to act on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Link: &amp;lt;/app.css&amp;gt;; rel=preload; as=style&lt;/code&gt; (also &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;font&lt;/code&gt;, or &lt;code&gt;rel=preconnect&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;HTTP/2 or HTTP/3 in practice — Chromium ignores Early Hints sent over HTTP/1.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Top-level &lt;strong&gt;navigations&lt;/strong&gt; only — a &lt;code&gt;103&lt;/code&gt; ahead of a &lt;code&gt;fetch()&lt;/code&gt; or XHR call does nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;td&gt;Page JavaScript &lt;strong&gt;cannot observe&lt;/strong&gt; a &lt;code&gt;103&lt;/code&gt; at all. The only ways to see it are &lt;code&gt;curl -v --http2&lt;/code&gt; or a browser's own debugging protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;The final &lt;code&gt;200&lt;/code&gt; should repeat the same &lt;code&gt;Link&lt;/code&gt; hints the &lt;code&gt;103&lt;/code&gt; sent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's spec'd as &lt;a href="https://www.rfc-editor.org/rfc/rfc8297" rel="noopener noreferrer"&gt;RFC 8297&lt;/a&gt;, and Chrome's own guidance is blunt about the tradeoff: it's not useful if your server can already send a &lt;code&gt;200&lt;/code&gt; immediately. The whole benefit is overlapping browser work with server think-time — nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequence diagram of a 103 Early Hints response arriving before the final 200&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h3384vckhuxhutknjsr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0h3384vckhuxhutknjsr.png" alt="Sequence diagram of a 103 Early Hints response arriving before the final 200" width="800" height="1612"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🛠️ What I built
&lt;/h2&gt;

&lt;p&gt;I put together &lt;a href="https://github.com/happyvig/http-103-early-hints-playground" rel="noopener noreferrer"&gt;a minimal Node app&lt;/a&gt; with one job: make the &lt;code&gt;103&lt;/code&gt; observable, not theoretical.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real &lt;code&gt;http2.createSecureServer()&lt;/code&gt; that emits a genuine informational &lt;code&gt;103&lt;/code&gt; response — not a mocked header.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/demo&lt;/code&gt; route with a &lt;code&gt;hints=1&lt;/code&gt; / &lt;code&gt;hints=0&lt;/code&gt; toggle, so the exact same page can be requested with the mechanism on or off.&lt;/li&gt;
&lt;li&gt;A server-side timeline log, because I learned early on that the browser's own Resource Timing API has a &lt;a href="https://github.com/w3c/resource-timing/issues/273" rel="noopener noreferrer"&gt;documented ambiguity&lt;/a&gt; for early-hinted resources — so I wanted a source of truth that couldn't be misread.&lt;/li&gt;
&lt;li&gt;A side-by-side waterfall comparison, so the difference is visible without needing to know how to read DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt;-verifiable output, so anyone can confirm the raw protocol independently of what any particular browser reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The demo app's home screen with the WITH/WITHOUT Early Hints controls&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsd3sle5fkb9yndb83qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsd3sle5fkb9yndb83qq.png" alt="The demo app's home screen with the WITH/WITHOUT Early Hints controls" width="641" height="1576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server intentionally has zero runtime npm dependencies — just Node's built-in &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, and &lt;code&gt;http2&lt;/code&gt; — because I wanted the protocol itself to be the only variable.&lt;/p&gt;

&lt;p&gt;The core of it is a handful of lines. Send the hint, do the slow work, then send the real response repeating the same &lt;code&gt;Link&lt;/code&gt; headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hintsEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeEarlyHints&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/styles/demo.css?s=...&amp;gt;; rel=preload; as=style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/scripts/demo.js?s=...&amp;gt;; rel=preload; as=script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;/images/hero.svg?s=...&amp;gt;; rel=preload; as=image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// simulated DB / SSR think-time&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* same Link headers, repeated */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the raw protocol, unedited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--http2&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="s2"&gt;"https://localhost:8103/demo?hints=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;lt; &lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;103&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; link: &amp;lt;/styles/demo.css?s=...&amp;gt;; rel=preload; as=style, &amp;lt;/scripts/demo.js?s=...&amp;gt;; rel=preload; as=script, ...&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; HTTP/2 200&lt;/span&gt;
&lt;span class="s"&gt;&amp;lt; content-type: text/html; charset=utf-8&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;103&lt;/code&gt; arrives immediately; the real &lt;code&gt;200&lt;/code&gt; follows a couple of seconds later, once the simulated backend work finishes. That gap is the entire point of the feature — it's dead time the browser can now spend usefully instead of idly.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 What I observed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It works exactly as documented, but the obvious way to verify it is misleading.&lt;/strong&gt; My first instinct was to compare &lt;code&gt;startTime&lt;/code&gt; timestamps between when the &lt;code&gt;103&lt;/code&gt; arrived and when the resource loaded. That number looked unconvincing on its own — sometimes single-digit milliseconds, sometimes negative. It took some digging to understand why: modern Chrome folds the &lt;code&gt;103&lt;/code&gt;'s arrival into &lt;code&gt;nav.responseStart&lt;/code&gt;, so it stops being a usable anchor point, and for a resource that's already sitting in cache by the time the page asks for it, the browser's recorded "start" time collapses to roughly when the page &lt;em&gt;parsed&lt;/em&gt; the reference, not when the fetch happened.&lt;/p&gt;

&lt;p&gt;The signal that actually proves the hint was honored is a different combination: &lt;code&gt;initiatorType&lt;/code&gt;, &lt;code&gt;fromDiskCache&lt;/code&gt;, and &lt;code&gt;transferSize&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;WITH hints&lt;/th&gt;
&lt;th&gt;WITHOUT hints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initiatorType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;early-hints&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;link&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fromDiskCache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transferSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; bytes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;3757&lt;/code&gt; bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;123ms&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;transferSize: 0&lt;/code&gt; combined with &lt;code&gt;fromDiskCache: true&lt;/code&gt; is the tell: the browser never went to the network for that stylesheet when the page asked for it, because the &lt;code&gt;103&lt;/code&gt; had already fetched it into cache while the server was still "thinking."&lt;/p&gt;

&lt;p&gt;The same story, visually — the waterfall with hints on shows the critical assets resolving almost instantly, sitting inside the server's think-time instead of after it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waterfall comparison, Early Hints OFF&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl139od5sehqvevey7ucy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl139od5sehqvevey7ucy.png" alt="Waterfall comparison, Early Hints OFF" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waterfall comparison, Early Hints ON&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwowad9j32ci155cz5pl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzwowad9j32ci155cz5pl.png" alt="Waterfall comparison, Early Hints ON" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirming the hint fired is not the same as confirming it mattered.&lt;/strong&gt; Those are two separate claims, and conflating them is an easy mistake to make. So I ran an interleaved A/B test — 20 runs per condition, fresh cache each time — and measured the metrics that describe actual user experience:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;WITH hints (mean)&lt;/th&gt;
&lt;th&gt;WITHOUT hints (mean)&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;2059ms&lt;/td&gt;
&lt;td&gt;2162ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;103ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Welch's t ≈ 21.6 — overwhelming (p ≪ 0.0001)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;2306ms&lt;/td&gt;
&lt;td&gt;2408ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;102ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Overwhelming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOMContentLoaded&lt;/td&gt;
&lt;td&gt;2034ms&lt;/td&gt;
&lt;td&gt;2130ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three metrics converge on roughly the same 100ms delta — which lines up almost exactly with this demo's own simulated critical-asset latency. That internal consistency is what makes the result trustworthy rather than coincidental.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline visualization showing Early Hints overlapping server think-time&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6nv1z766eusqaqit9k8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6nv1z766eusqaqit9k8.png" alt="Timeline visualization showing Early Hints overlapping server think-time" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The benefit scales with network latency, which is exactly what the theory predicts.&lt;/strong&gt; I re-ran the same test under throttled network profiles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;RTT&lt;/th&gt;
&lt;th&gt;FCP delta (WITHOUT − WITH)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No throttling&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;108ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast 4G&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;td&gt;112ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow 4G&lt;/td&gt;
&lt;td&gt;170ms&lt;/td&gt;
&lt;td&gt;224ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3G&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;td&gt;468ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a fast connection, Early Hints saves about 100ms. On a slow mobile connection, that grows to nearly half a second. Early Hints is fundamentally an RTT-savings mechanism — the worse your users' connections, the more it's worth having.&lt;/p&gt;

&lt;p&gt;⚠️ Two honest caveats before anyone takes these numbers further than they should go. First, the think-time and asset latency here are synthetic — a real app streams HTML, sits behind a CDN, and has a messier mix of cache states than a clean lab run. This is evidence that the &lt;em&gt;mechanism&lt;/em&gt; works and scales with RTT, not a promise of an identical win on any particular product. Second, Early Hints only helps on a cold-ish cache — once a returning visitor already has the assets cached from a prior visit, there's nothing left for the &lt;code&gt;103&lt;/code&gt; to save. Both are reasons to treat this as lab data, not a substitute for real field measurement (more on that below).&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ The ideal setup
&lt;/h2&gt;

&lt;p&gt;Based on everything above, here's the shape of a deployment where Early Hints is worth using at all:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Meaningful server think-time.&lt;/strong&gt; If your time-to-first-byte is already under roughly 100–200ms, there's nothing for Early Hints to fill — Chrome's own guidance says as much. It pays off when the server genuinely needs a beat to do its work (a slow API call, a computed template, an origin fetch).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A render-blocking resource that isn't already discoverable early.&lt;/strong&gt; The &lt;code&gt;103&lt;/code&gt; is only useful for something the browser wouldn't otherwise learn about until late — a stylesheet or font referenced deep in a server-rendered response, not something already sitting at the top of a streamed &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real HTTPS, properly provisioned.&lt;/strong&gt; This is worth calling out explicitly: the mechanism relies on the browser's HTTP cache, and browsers are stricter about caching on connections they don't fully trust. In production, this is a non-issue as long as TLS is terminated with a properly issued certificate — your CDN, load balancer, or platform's managed TLS all satisfy this by default. It only becomes a footnote if you're experimenting on &lt;code&gt;localhost&lt;/code&gt; with a self-signed cert, which is a local-tooling detail, not a production concern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A request path that passes &lt;code&gt;1xx&lt;/code&gt; responses through untouched.&lt;/strong&gt; Any reverse proxy, CDN, or load balancer sitting in front of your app needs to forward informational responses rather than stripping them. This is the single most common way Early Hints silently fails in production — the server sends a correct &lt;code&gt;103&lt;/code&gt;, and something in front of it never lets it reach the browser. Confirm this with your specific infrastructure before assuming it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable, non-drifting asset URLs between the &lt;code&gt;103&lt;/code&gt; and the &lt;code&gt;200&lt;/code&gt;.&lt;/strong&gt; If your asset URLs are content-hashed or versioned, make sure the version referenced in the &lt;code&gt;103&lt;/code&gt; matches what the final &lt;code&gt;200&lt;/code&gt; will reference — a mismatch either wastes the preload or, worse, risks serving a stale asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧭 Guidelines if you want to try this in your own app
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What to do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Emit the &lt;code&gt;103&lt;/code&gt; as early as your server can determine the resource list — typically right after routing, before any slow work begins.&lt;/li&gt;
&lt;li&gt;Keep the hinted resource list to what's genuinely critical (main stylesheet, a key font, maybe a hero image). Hinting everything defeats the purpose and can even compete with the final response for bandwidth.&lt;/li&gt;
&lt;li&gt;Match the &lt;code&gt;103&lt;/code&gt;'s &lt;code&gt;Link&lt;/code&gt; headers to the final &lt;code&gt;200&lt;/code&gt;'s &lt;code&gt;Link&lt;/code&gt;/&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags exactly, including query strings or hashes.&lt;/li&gt;
&lt;li&gt;Only apply this to top-level page navigations — a &lt;code&gt;103&lt;/code&gt; ahead of an API call or client-side &lt;code&gt;fetch()&lt;/code&gt; has no effect, since Early Hints is a navigation-time mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where to do it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTTP/2 or HTTP/3 origins are the natural fit — this demo intentionally uses &lt;code&gt;http2.createSecureServer()&lt;/code&gt; because informational responses are a well-supported, first-class part of the protocol there.&lt;/li&gt;
&lt;li&gt;Whatever sits in front of your origin (CDN, reverse proxy, load balancer) needs to forward &lt;code&gt;1xx&lt;/code&gt; responses rather than strip them — see "the ideal setup" above for why this is the most common way the whole thing silently fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧪 A testing checklist you can reuse
&lt;/h2&gt;

&lt;p&gt;Everything in this post rests on one habit: never let one check stand in for&lt;br&gt;
all three questions that actually matter — &lt;strong&gt;did the server send it, did the&lt;br&gt;
browser act on it, and did it help anyone.&lt;/strong&gt; Here's that as a checklist, in the&lt;br&gt;
order to actually run it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the wire protocol.&lt;/strong&gt; Run &lt;code&gt;curl -v --http2 &amp;lt;url&amp;gt;&lt;/code&gt; against the real&lt;br&gt;
environment you care about — staging or production, not just localhost.&lt;br&gt;
Look for &lt;code&gt;&amp;lt; HTTP/2 103&lt;/code&gt; ahead of the final &lt;code&gt;&amp;lt; HTTP/2 200&lt;/code&gt;. No &lt;code&gt;103&lt;/code&gt;? Stop&lt;br&gt;
here — the bug is the server or something in front of it (most often a&lt;br&gt;
proxy/CDN stripping &lt;code&gt;1xx&lt;/code&gt; responses), and nothing downstream will fix that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the browser acted, not just received.&lt;/strong&gt; Use Chrome DevTools&lt;br&gt;
Protocol or Firefox's WebDriver BiDi to watch for&lt;br&gt;
&lt;code&gt;Network.responseReceivedEarlyHints&lt;/code&gt;, then check the hinted resource's&lt;br&gt;
&lt;code&gt;initiatorType&lt;/code&gt; (&lt;code&gt;early-hints&lt;/code&gt;), &lt;code&gt;fromDiskCache&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt;), and&lt;br&gt;
&lt;code&gt;transferSize&lt;/code&gt; (&lt;code&gt;0&lt;/code&gt;). Skip raw timestamps — they're misleading for&lt;br&gt;
early-hinted resources, for reasons explained earlier in this post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run a real A/B, not a single load.&lt;/strong&gt; One page load proves nothing —&lt;br&gt;
network jitter alone swings FCP by tens of milliseconds. Interleave at&lt;br&gt;
least ~15–20 cold runs per condition (hints on/off) and compare FCP, LCP,&lt;br&gt;
and DOMContentLoaded means, not a single number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throttle the network and re-check.&lt;/strong&gt; The benefit should &lt;em&gt;grow&lt;/em&gt; as RTT&lt;br&gt;
grows, and shrink toward zero on a fast, low-latency connection — that's&lt;br&gt;
the signature of a genuine network effect. If the delta doesn't move with&lt;br&gt;
latency, be skeptical of the result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check the warm-cache case too.&lt;/strong&gt; Early Hints should do nothing once a&lt;br&gt;
resource is already fresh in cache. If your "win" doesn't disappear on a&lt;br&gt;
warm cache, you're not measuring what you think you're measuring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate against real traffic before calling it a win.&lt;/strong&gt; Every step above&lt;br&gt;
runs in a lab. Ship it behind a flag, split real users with &lt;code&gt;web-vitals&lt;/code&gt; or&lt;br&gt;
your existing RUM pipeline, and compare distributions over a real sample.&lt;br&gt;
This is the only step that answers "was this worth it," not just "does it&lt;br&gt;
work."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't skip ahead to step 6 to settle an argument faster — if step 1 or 2 is&lt;br&gt;
quietly broken (an untrusted local cert, a proxy eating &lt;code&gt;1xx&lt;/code&gt;), every step&lt;br&gt;
after it is measuring nothing. Full write-up, exact commands, and this repo's&lt;br&gt;
own scripts for steps 2–5 are in &lt;a href="https://github.com/happyvig/http-103-early-hints-playground/blob/main/TESTING.md" rel="noopener noreferrer"&gt;&lt;code&gt;TESTING.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you ship it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Make it a feature flag or config toggle, not a hardcoded behavior. If the passthrough assumption above turns out wrong on some edge of your infrastructure (a specific CDN region, a legacy load balancer), you want to switch it off without a deploy.&lt;/li&gt;
&lt;li&gt;Treat this as a targeted optimization, not a default. It earns its complexity only when the conditions in "the ideal setup" above are all true; outside of that, it's easy to add risk (cache-poisoning surface, extra server logic) for no measurable gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 Where to go from here
&lt;/h2&gt;

&lt;p&gt;Everything above — the raw benchmark scripts, the full protocol walkthrough, and the architecture notes for the demo itself — lives in &lt;a href="https://github.com/happyvig/http-103-early-hints-playground/blob/main/TECH_NOTES.md" rel="noopener noreferrer"&gt;&lt;code&gt;TECH_NOTES.md&lt;/code&gt;&lt;/a&gt; in the repo, if you want to reproduce any of it or adapt it to your own stack.&lt;/p&gt;

&lt;p&gt;I started this because I was tired of taking "it improves FCP" on faith. What I ended up with wasn't just a working demo — it was a much better sense of exactly where this feature earns its keep and where it's dead weight. That's the part no diagram gives you. If you're weighing whether to build this into a real app, my honest advice is the same thing that got me here: stop reading about it, and go watch it happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8297" rel="noopener noreferrer"&gt;RFC 8297 — Early Hints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/web-platform/early-hints" rel="noopener noreferrer"&gt;Chrome DevRel — Early Hints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/w3c/resource-timing/issues/273" rel="noopener noreferrer"&gt;W3C Resource Timing — issue #273&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/early-hints/" rel="noopener noreferrer"&gt;Early Hints: How Cloudflare Can Improve Website Load Times by 30%&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.nginx.org/blog/nginx-introduces-support-103-early-hints" rel="noopener noreferrer"&gt;Nginx support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>http</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
