Early Hints and Speculation Rules for Media
Every resource hint discussed elsewhere in the Lazy Loading, Preloading & Fetch Priorities reference lives inside the HTML document, which means none of them can fire until the origin has finished rendering that document and the first bytes of the body have reached the client. HTTP 103 Early Hints and the Speculation Rules API are the two mechanisms that break that ordering constraint: the first lets the server emit Link: rel=preload headers before the 200 response exists, and the second lets the browser fetch — or fully render — the next page before the user has navigated to it. Both are unusually effective for media, because hero images and poster frames are large, discovered late, and sit directly on the Largest Contentful Paint critical path.
This guide covers the wire format of a 103 response, how Chromium consumes it, what the Speculation Rules API actually downloads when it prerenders a media-heavy page, how Cloudflare and Fastly generate hints at the edge, and how the two features interact with fetchpriority and rel=preload once they land in the same document.
Concept & Architecture: What a 103 Response Actually Is
HTTP 103 Early Hints (RFC 8297) is an informational status code. Informational responses — the 1xx class — are not the final response to a request; a client that receives one keeps the request open and waits for the eventual final status. A server may send any number of 103 responses before the 200, each carrying a header block. The only headers with defined meaning in a 103 are Link headers, and the only rel values Chromium acts on are preload, preconnect, modulepreload and dns-prefetch.
The value of the mechanism is entirely a function of origin think time — the interval between the origin receiving the request and being able to write the first byte of the HTML body. On a static file server that interval is a few milliseconds and Early Hints buys you nothing. On a server-rendered application that queries a database, renders a template and calls three internal services, it is routinely 150–600 ms. That whole window is dead air on the connection: the TCP/QUIC connection is established, the TLS handshake is done, congestion window is warm, and the browser is idle. A 103 fills it with the hero image fetch.
The critical architectural detail is that the browser processes the hints on the navigation request’s connection, in the navigation context, before a document exists. There is no DOM, no CSSOM, no preload scanner — the hinted resources go straight into the preload cache keyed by URL, destination, CORS mode and credentials mode. When the 200 arrives and the parser reaches the corresponding <img> or <link rel=preload>, the fetch is already in flight or complete, and the parser adopts the existing record instead of issuing a new request.
The sequence below shows the timing for a page with a 220 ms origin think time and a 180 KB AVIF hero.
Three constraints follow directly from this architecture and account for most failed rollouts. First, Chromium only processes Early Hints for main-frame navigation requests — hints attached to a subresource, an iframe navigation, or an XHR are discarded. Second, only the first 103 in a response chain is acted upon; subsequent ones are ignored, so a CDN that appends its own 103 after the origin’s will find its hints dropped. Third, a 103 is not cacheable in its own right — it is replayed by the CDN from a stored association between the URL and the Link headers observed on a previous 200, which is exactly how Cloudflare’s implementation works.
The Speculation Rules API and Its Media Cost
Speculation Rules attack the same latency from the opposite side of the navigation. A <script type="speculationrules"> block declares URLs the browser may fetch (prefetch) or fully render off-screen (prerender) ahead of a click. On activation the prerendered document is swapped into the tab, and paint is effectively instantaneous.
The distinction that matters for media delivery is what each action downloads:
prefetchissues a single request for the document HTML withSec-Purpose: prefetch. It does not run scripts, does not parse for subresources, and does not fetch a single image. Typical cost: 10–40 KB.prerendercreates a hidden renderer, parses the document, executes JavaScript, applies CSS and fetches every subresource the page needs — including the full responsive image set thesrcsetandsizesattributes resolve to for the current viewport. Typical cost on a media-heavy landing page: 400 KB to 2 MB.
That asymmetry is the entire budgeting problem. Prerendering three product pages on a photo-led catalogue can transfer more bytes than the user’s whole session, and the browser will happily do it on a metered connection unless you constrain the rules. Chromium applies its own ceilings — 10 concurrent prerenders and 50 prefetches at immediate/eager eagerness, and 2 of each at moderate/conservative — but those are limits on count, not on bytes, so a small number of heavy pages still blows the budget.
Two more prerender behaviours are specific to media and routinely surprise people. Autoplaying <video> does not start while the document is prerendering; playback is deferred until activation, which is the correct behaviour but breaks any code that measures playback start from DOMContentLoaded. And loading="lazy" images below the fold are not fetched during prerender, because the hidden renderer uses the eventual viewport for intersection calculations — so native lazy loading is the single most effective lever you have for keeping the prerender payload small.
Benchmark Data
The figures below come from repeated WebPageTest runs against a server-rendered catalogue page: 4G profile at 20 Mbps down and 40 ms RTT, a 180 KB AVIF hero at 1600w, 14 additional subresources, and an origin think time held at 220 ms by a synthetic delay. Each row is the median of nine runs; the byte column is total transfer for the navigation.
| Configuration | LCP (ms) | Hero request start (ms) | Bytes on navigation |
|---|---|---|---|
| No hints at all | 2 450 | 640 | 512 KB |
<link rel=preload> in HTML <head> |
2 180 | 395 | 512 KB |
| 103 Early Hints preloading the hero | 1 790 | 55 | 512 KB |
103 Early Hints + fetchpriority=high |
1 660 | 48 | 512 KB |
Early Hints + preconnect to the media origin |
1 605 | 45 | 512 KB |
Speculation Rules prerender, measured from activation |
210 | −1 940 (pre-navigation) | 512 KB × N speculated |
Key finding: Early Hints removed 390 ms of LCP relative to an in-document preload, and essentially all of that came from the hero request starting at 55 ms instead of 395 ms. The subsequent fetchpriority and preconnect refinements are worth another 185 ms combined but are second-order — get the 103 landing first. Prerender is in a different category entirely: it does not make the page faster, it makes the page already loaded, at the cost of transferring the whole payload for every speculated URL whether or not the user clicks.
CDN and Server Support Matrix
| Platform | 103 emission | How hints are produced |
|---|---|---|
| Cloudflare | Yes | Harvests Link headers from a previous 200 for the URL, caches them, replays as 103 on subsequent requests. Toggle under Speed → Optimization. |
| Fastly | Yes | Explicit h2.early_hints() call in VCL, or a Compute service writing an informational response. Fully programmatic. |
| Akamai | Yes | Origin-driven; Link headers on the 200 are promoted to a 103 by the Early Hints behaviour. |
| AWS CloudFront | No native 103 | CloudFront does not forward informational responses; use in-document preload or Lambda@Edge-generated Link headers on the 200. |
| nginx | No | No 103 support in the mainline HTTP core; terminate at a proxy that can emit one. |
| Node.js ≥ 18.11 | Yes | response.writeEarlyHints({ link: [...] }) on both HTTP/1.1 and HTTP/2. |
Caddy / Go net/http |
Yes | http.ResponseWriter.WriteHeader(http.StatusEarlyHints) after setting Link. |
Warning: Early Hints require HTTPS in every shipping browser implementation, and Chromium additionally ignores 103 over HTTP/1.0. In practice this means HTTP/2 or HTTP/3 over TLS, which is what any CDN-fronted origin already serves. Behind a reverse proxy the informational response must be forwarded verbatim — a proxy that buffers until the final status silently destroys the entire benefit while leaving the 200 intact, so nothing looks broken.
Step-by-Step Implementation
Step 1 — Decide Whether Early Hints Can Help At All
Measure origin think time before writing a line of config. If the origin returns the first HTML byte in under 60 ms there is no window to fill and the added header round trip is pure overhead.
#!/usr/bin/env bash
# Separates connection setup from origin think time. The number that matters is
# (time_starttransfer - time_appconnect): TLS is done, so everything left is the
# server building the response. Bypass the CDN cache to measure the real origin.
URL="https://example.com/"
curl -sS -o /dev/null --http2 \
-H 'Cache-Control: no-cache' \
-w 'dns %{time_namelookup}\ntcp %{time_connect}\ntls %{time_appconnect}\nttfb %{time_starttransfer}\nthink_time %{time_starttransfer}\n' \
"$URL"
# Repeat nine times and take the median; a single sample is meaningless on a
# warm/cold cache boundary.
for i in $(seq 1 9); do
curl -sS -o /dev/null --http2 -H 'Cache-Control: no-cache' \
-w '%{time_starttransfer} %{time_appconnect}\n' "$URL"
done | awk '{ printf "%.0f ms think time\n", ($1 - $2) * 1000 }' | sort -n
If the median think time is above roughly 100 ms, proceed.
Step 2 — Emit the 103 From the Origin
The origin is the only component that knows which hero asset a given route will render, so it should be the source of truth. Node 18.11+ exposes writeEarlyHints directly on the response object.
// server.mjs — run with: node server.mjs (HTTPS required in real deployments)
import http from 'node:http';
const HERO = {
'/': '/media/home-hero-1600.avif',
'/catalogue': '/media/catalogue-hero-1600.avif'
};
const server = http.createServer(async (req, res) => {
const hero = HERO[req.url];
if (hero) {
// Emitted BEFORE any database or template work. Each array entry becomes one
// Link header in the 103 block.
res.writeEarlyHints({
link: [
// as=image is mandatory: without a destination the browser cannot match
// the preload record to the later <img> and will fetch the file twice.
`<${hero}>; rel=preload; as=image; fetchpriority=high; ` +
// imagesrcset/imagesizes must be byte-identical to the <img> attributes,
// otherwise the candidate the parser selects may not be the one preloaded.
`imagesrcset="${hero.replace('1600', '800')} 800w, ${hero} 1600w"; ` +
`imagesizes="(max-width: 800px) 100vw, 1600px"`,
// Warm the TLS connection to the media CDN for everything not hinted.
'<https://media.example.com>; rel=preconnect; crossorigin'
]
});
}
// ---- expensive work happens here: 220 ms of queries and rendering ----
const html = await renderPage(req.url);
res.writeHead(200, {
'content-type': 'text/html; charset=utf-8',
// Repeat the same Link header on the 200 so CDNs that harvest hints from the
// final response (Cloudflare) can learn and replay them.
link: hero ? `<${hero}>; rel=preload; as=image` : ''
});
res.end(html);
});
async function renderPage(url) {
await new Promise(r => setTimeout(r, 220)); // stand-in for real render cost
return `<!doctype html><html><head><title>Demo</title></head><body>
<img src="/media/home-hero-1600.avif"
srcset="/media/home-hero-800.avif 800w, /media/home-hero-1600.avif 1600w"
sizes="(max-width: 800px) 100vw, 1600px"
fetchpriority="high" width="1600" height="900" alt="Hero">
</body></html>`;
}
server.listen(8080);
Step 3 — Enable Replay at the Cloudflare Edge
Cloudflare does not invent hints; it caches the Link headers it sees on a 200 and replays them as a 103 for later requests to the same URL. That means Step 2’s duplicate Link header on the final response is a prerequisite, not a nicety.
#!/usr/bin/env bash
# Enable Early Hints for a zone via the API. ZONE_ID and CF_API_TOKEN come from
# the environment; the token needs Zone Settings:Edit.
curl -sS -X PATCH \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/settings/early_hints" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"value":"on"}' | jq '.result'
# Verify the replay is live. --http2 is required: informational responses are not
# surfaced by curl over HTTP/1.1 in older builds, and 103 is HTTPS-only anyway.
curl -sSI --http2 https://example.com/ 2>&1 | grep -iE '^(HTTP/2 103|HTTP/2 200|link:)'
# Expect:
# HTTP/2 103
# link: </media/home-hero-1600.avif>; rel=preload; as=image
# HTTP/2 200
Note that the first request to a cold URL sees no 103 — Cloudflare must observe a 200 first. Full walkthrough including cache-key interactions and the cf-cache-status semantics is in enabling 103 Early Hints on Cloudflare for hero images.
Step 4 — Emit Hints Programmatically From Fastly VCL
Fastly is the opposite model: nothing is harvested, you call h2.early_hints() explicitly and it fires immediately, before the backend is even contacted. That makes it the better fit when the hero URL is derivable from the request path.
sub vcl_recv {
#FASTLY recv
# Only main-document navigations benefit; hinting on subresources is discarded
# by the browser and wastes header bytes.
if (req.method == "GET" && req.http.Accept ~ "text/html") {
if (req.url.path == "/") {
# Each call adds one header line to the 103 block. Fires before the
# backend fetch, so it costs zero origin latency.
h2.early_hints("link: ; rel=preload; as=image; fetchpriority=high");
h2.early_hints("link: ; rel=preconnect; crossorigin");
}
# Derive the hero from the route for templated pages.
if (req.url.path ~ "^/product/([a-z0-9-]+)/?$") {
h2.early_hints("link: ; rel=preload; as=image");
}
}
}
Warning: h2.early_hints() fires unconditionally, including on requests that will end in a 404 or a 302. A redirect that carries a preload hint for the old page’s hero downloads bytes the user will never see. Gate the calls on route patterns you know resolve to a rendered document, exactly as above. The same header-manipulation discipline covered in Fastly VCL for image format negotiation applies here.
Step 5 — Add Speculation Rules With a Media Budget
Rules go in a <script type="speculationrules"> block, or in a JSON document referenced by the Speculation-Rules response header. Use document rules with where clauses so links are matched dynamically rather than enumerated.
<script type="speculationrules">
{
"prerender": [{
"source": "document",
"where": {
"and": [
{ "href_matches": "/product/*" },
// Exclude anything that would blow the byte budget or mutate state.
{ "not": { "href_matches": "/product/*/gallery*" } },
{ "not": { "selector_matches": "[data-heavy-media]" } },
{ "not": { "selector_matches": "[rel~=nofollow]" } }
]
},
// moderate = fire after ~200 ms of hover. Cheapest signal that still
// wins the perceived-latency race, and capped at 2 concurrent prerenders.
"eagerness": "moderate"
}],
"prefetch": [{
"source": "document",
"where": { "href_matches": "/catalogue/*" },
// Document-only fetch: safe to be more aggressive because no media moves.
"eagerness": "eager",
// Treat these query params as not affecting the response, so a prefetch of
// ?utm_source=x can still serve a click on the clean URL.
"expects_no_vary_search": "params=(\"utm_source\" \"utm_medium\")"
}]
}
</script>
<script>
// Withdraw prerendering on constrained networks. Rule sets are inert once
// removed from the DOM, so deleting the node cancels pending speculations.
const conn = navigator.connection;
if (conn && (conn.saveData || /2g/.test(conn.effectiveType))) {
document.querySelectorAll('script[type="speculationrules"]').forEach(n => n.remove());
}
</script>
The concurrency ceilings, byte accounting, and how to keep a prerendered media page under a fixed budget are worked through in speculation rules prerender and the media prefetch budget.
Step 6 — Make the Prerendered Page Media-Safe
A prerendered document runs its JavaScript before the user has committed to visiting it. Analytics fire early, autoplay is deferred, and any media player that starts buffering on load will pull segments for a page that may never be shown.
// Gate expensive media work until the document is actually activated.
function onActivated(fn) {
if (document.prerendering) {
// Fires exactly once, when the hidden renderer is swapped into the tab.
document.addEventListener('prerenderingchange', fn, { once: true });
} else {
fn();
}
}
onActivated(() => {
// Start the HLS/DASH player only now — segment requests during prerender are
// pure waste if the click never comes.
document.querySelectorAll('video[data-src]').forEach(v => {
v.src = v.dataset.src;
v.load();
});
sendPageView();
});
// Server side: skip heavy media rendering for speculative requests entirely.
// Sec-Purpose: prefetch → document-only prefetch
// Sec-Purpose: prefetch;prerender → full prerender
// Respond 503 to opt a route out of speculation altogether.
Step 7 — Measure the LCP Delta Correctly
For a prerendered page the clock the browser reports and the clock the user perceives are different. PerformanceNavigationTiming.activationStart is the offset at which activation happened; every timing before it happened while the page was invisible and must be subtracted.
// Reports the LCP the user actually experienced, correct for both normal and
// prerendered navigations, and tags each beacon with the hint path that produced it.
const nav = performance.getEntriesByType('navigation')[0];
const activationStart = nav?.activationStart ?? 0;
new PerformanceObserver(list => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
// Clamp at 0: on a prerendered page LCP often occurred BEFORE activation.
const lcp = Math.max(0, last.startTime - activationStart);
// Did the hero come from an Early Hints preload? initiatorType 'early-hints'
// is reported by Chromium for resources started by a 103.
const hero = performance.getEntriesByName(last.url || '')[0];
navigator.sendBeacon('/rum', JSON.stringify({
lcp: Math.round(lcp),
element: last.element?.tagName,
prerendered: activationStart > 0,
initiator: hero?.initiatorType, // 'early-hints' | 'img' | 'link'
renderDelay: Math.round(last.startTime - (hero?.responseEnd ?? last.startTime))
}));
}).observe({ type: 'largest-contentful-paint', buffered: true });
Field-level attribution — separating the population that received a 103 from the population that did not — is covered in measuring the LCP impact of Early Hints, and the aggregate view over 28 days comes from the CrUX API.
Parameter Reference
Link header parameters honoured in a 103
| Parameter | Values | Notes |
|---|---|---|
rel |
preload, preconnect, modulepreload, dns-prefetch |
Anything else in a 103 is ignored by Chromium. prefetch is not processed from Early Hints. |
as |
image, video, audio, font, style, script, fetch |
Mandatory for preload. Wrong or missing as produces a duplicate fetch and an unused-preload console warning. |
imagesrcset / imagesizes |
srcset / sizes syntax | Must match the <img> attributes exactly, including whitespace-normalised descriptors, or the browser preloads a candidate it will not use. |
fetchpriority |
high, low, auto |
Chromium honours this in Link headers; other engines ignore it harmlessly. |
crossorigin |
present, anonymous, use-credentials |
Must match the element’s CORS mode. A mismatch partitions the cache and fetches the asset twice. |
media |
media query | Evaluated against the viewport before the document exists — safe, because the browser already knows the window size. |
type |
MIME type | Lets the browser skip a hint for a format it cannot decode, e.g. type="image/avif" on a legacy engine. |
nopush |
flag | Legacy HTTP/2 push suppressant. Harmless, and irrelevant now that push is removed from Chromium. |
Speculation rules fields
| Field | Values | Notes |
|---|---|---|
source |
list, document |
list enumerates urls; document matches links in the page via where. |
eagerness |
immediate, eager, moderate, conservative |
immediate fires on parse; eager on any hover intent; moderate after ~200 ms hover; conservative on pointerdown. |
where |
href_matches, selector_matches, and, or, not |
URL patterns support * wildcards and named groups; selectors are matched against the anchor element. |
requires |
anonymous-client-ip-when-cross-origin |
Only meaningful for cross-origin prefetch through a privacy-preserving proxy. |
referrer_policy |
any Referrer-Policy token | Applied to the speculative request; a stricter policy than the document’s is required for some cross-origin cases. |
expects_no_vary_search |
No-Vary-Search header syntax |
Declares which query parameters do not change the response, letting one speculation satisfy several URLs. |
tag |
free string | Labels the rule set; surfaced in the Sec-Speculation-Tags request header for server-side attribution. |
Tradeoffs & Edge Cases
Tradeoff: Early Hints spends bandwidth on a guess, and the guess is per-URL, not per-user. The hinted hero is fetched before the origin has decided anything — including whether the user is authenticated, A/B bucketed, or about to be redirected. If half your traffic on / is bucketed into a variant with a different hero, half your Early Hints traffic is downloading an image that is never painted. Hint only assets that are invariant across every variant of the route, or move hint generation into an edge worker that can read the bucketing cookie before emitting the 103.
Tradeoff: prerender is a bandwidth multiplier with a hit-rate denominator. A prerender that converts is close to free from the user’s perspective; one that does not converts a 480 KB page load into pure waste. At moderate eagerness on a typical catalogue, hover-to-click conversion runs around 45–65%, so expect to transfer roughly 1.7× your normal media bytes for the pages under rules. On a photo-heavy site that is the difference between a 3 MB and a 5 MB session — audit it against real numbers before shipping, and prefer prefetch for anything whose value is mostly the HTML.
Warning: a mismatched crossorigin between the 103 hint and the element double-fetches the hero. This is the single most common Early Hints bug and it is invisible in aggregate metrics: LCP does not regress, bytes silently double. The preload record is keyed by URL plus destination plus CORS mode plus credentials mode. Link: </hero.avif>; rel=preload; as=image with a matching <img crossorigin="anonymous"> in the document produces two requests, because the hint’s mode is no-cors and the element’s is cors. Either add crossorigin to the hint or remove it from the element.
Edge case: hints replayed from cache go stale after a deploy. Cloudflare’s harvest-and-replay model stores the Link headers it observed on an earlier 200. Ship a new hashed hero filename and the edge keeps replaying the old URL until the stored association is purged, preloading a 404 on every navigation. Purge Early Hints alongside HTML on deploy, and keep the hinted asset URLs on a Cache-Control: immutable policy consistent with the rest of your cache-control strategy for media assets.
Edge case: Sec-Purpose handling determines whether prerender corrupts your analytics. A prerendered page executes analytics, ad, and consent scripts before the user has consented to anything by clicking. Chromium does defer some APIs, but a plain fetch() in a tag manager fires immediately. Every third-party script on a prerendered route needs auditing behind the document.prerendering gate from Step 6, or your funnel gains a large population of pageviews with zero engagement.
Edge case: Early Hints and <link rel=preload> in the document are complements, not alternatives. Keep both. The 103 serves the population on browsers and CDN paths that support it; the in-document preload covers everyone else, including Safari on connections that skip informational responses, and costs nothing extra when the hint already won — the parser adopts the in-flight record rather than issuing a second request. The same reasoning applies to rel=preconnect for CDN media origins: hint it in the 103 and keep the tag.
Debugging & Validation
Confirm the 103 is on the wire
curl prints informational responses when you ask for the full header trace. If you see only the 200, either the origin is not emitting, or something between it and you is swallowing the response.
# -v over HTTP/2 shows the 103 frame explicitly. grep keeps the output readable.
curl -sSv --http2 https://example.com/ -o /dev/null 2>&1 \
| grep -iE '< HTTP/2 (103|200)|< link:'
# HTTP/3 path — worth checking separately, some edges only implement 103 on h2.
curl -sSv --http3 https://example.com/ -o /dev/null 2>&1 | grep -iE '103|link:'
# Confirm the hinted asset actually exists. A hint pointing at a 404 costs a
# round trip on every navigation and never shows up in LCP metrics.
HINT=$(curl -sSI --http2 https://example.com/ | grep -i '^link:' \
| sed -E 's/.*<([^>]+)>.*/\1/' | head -1)
curl -sS -o /dev/null -w '%{http_code} %{size_download} bytes %{url_effective}\n' \
"https://example.com${HINT}"
Verify the browser used the hint, not a second request
In Chrome DevTools → Network, enable the Initiator column. A resource fetched from a 103 reports its initiator as early-hints. Two rows for the same URL means the preload record did not match — check crossorigin, as, and whether imagesrcset selected a different candidate.
// Paste into the console after a reload. Any URL appearing twice is a
// duplicate fetch caused by a hint/element mismatch.
const byUrl = {};
performance.getEntriesByType('resource').forEach(e => {
(byUrl[e.name] ??= []).push(e.initiatorType);
});
Object.entries(byUrl)
.filter(([, inits]) => inits.length > 1)
.forEach(([url, inits]) => console.warn('DUPLICATE', url, inits));
// Confirm which resources the 103 actually started.
performance.getEntriesByType('resource')
.filter(e => e.initiatorType === 'early-hints')
.forEach(e => console.log('from 103:', e.name, Math.round(e.responseEnd), 'ms'));
Inspect speculation state
Chrome DevTools ships a dedicated Application → Speculative loads panel listing every rule set, every candidate URL, and — critically — the reason a speculation was rejected. MemoryLimitExceeded, DataSaverEnabled, MaxNumOfRunning...PrerendersExceeded and TriggerDestroyed account for the large majority of silent no-ops. chrome://net-export/ capture replayed in the netlog viewer gives the same information at frame level when the panel is not enough.
# Headless check that the rule block parses. An invalid JSON body is dropped
# silently — no console error, no panel entry.
curl -sS https://example.com/ \
| python3 -c "
import sys, re, json
html = sys.stdin.read()
for m in re.finditer(r'<script type=\"speculationrules\">(.*?)</script>', html, re.S):
try:
rules = json.loads(m.group(1))
print('OK', list(rules.keys()), [r.get('eagerness') for v in rules.values() for r in v])
except json.JSONDecodeError as e:
print('INVALID speculation rules:', e)
"
Prove the LCP delta
Run the same URL twice through WebPageTest with the Early Hints setting toggled, and diff the filmstrips rather than trusting a single LCP number — the WebPageTest filmstrip diff automation workflow does this on a schedule. For lab confirmation of the request-start shift, the DevTools Performance panel’s Network track is decisive: with a working 103 the hero bar begins before the HTML bar ends, which is impossible for any in-document hint.
Related
- Enabling 103 Early Hints on Cloudflare for hero images — the harvest-and-replay model, cache keys, and purge behaviour on deploy
- Speculation rules prerender and the media prefetch budget — concurrency ceilings, byte accounting and eagerness tuning for image-heavy routes
- Measuring the LCP impact of Early Hints — field attribution that separates hinted from unhinted navigations
- Using fetchpriority to optimize critical media — the priority signal that pairs with a 103 preload hint
- Preload vs prefetch for video and image assets — the in-document hints that Early Hints promotes to the header layer
- Advanced IntersectionObserver patterns for media — viewport-driven scheduling for everything the header hints do not cover
- Lazy Loading, Preloading & Fetch Priorities — parent reference covering the full browser resource scheduling model