The browser parses HTML top to bottom and discovers the resources it needs one at a time. A font isn't requested until the CSS that references it has been fetched and parsed; the LCP image isn't requested until the markup that points to it shows up. A resource hint moves that discovery earlier — a single <link rel> line tells the browser "you'll need this soon, go grab it now." Used wrongly, though, it just wastes bandwidth and steals priority from the resources that actually matter. This guide covers what each hint in the family does, when to reach for it, and the mistakes people keep making.
Why hints exist — the discovery delay
<!-- The order the browser sees things -->
1. Receives HTML → starts parsing
2. Finds <link rel="stylesheet" href="app.css"> → requests CSS
3. CSS arrives → parses → finds @font-face url(...) → requests font ← here!
4. Finds <img src="hero.jpg"> → requests the LCP image
The font and the LCP image are discovered at the END of the chain.
Preload them and they fetch in parallel right after step 1 —
hundreds of ms saved.The whole point is shortening the critical chain: telling the browser before it would have figured things out on its own. For the critical path itself, see the how-browsers-render-pages guide.
dns-prefetch — just the DNS
<link rel="dns-prefetch" href="https://cdn.example.com">- Resolves only the DNS for that origin. No TCP, no TLS.
- The cheapest hint — almost free, and it doesn't commit to an actual connection.
- When — a less-critical third-party origin you might use soon (analytics, ads, a candidate font CDN). Cheap insurance when a full connection would be overkill.
- DNS lookups are typically 20-120 ms — more noticeable on mobile and slow networks.
preconnect — DNS + TCP + TLS
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>- Resolves DNS, opens the TCP handshake, and (over HTTPS) negotiates TLS ahead of time. It "warms up" the connection.
- When — a critical third-party origin you'lldefinitely use: a font host, an image CDN, an API origin.
- Mind crossorigin — fonts are fetched in anonymous CORS mode, so without the
crossoriginattribute the warmed connection isn't reused and the browser opens a second one. Font origins almost always needcrossorigin. - Don't overdo it — each origin ties up connection resources. Past 4-6 preconnects you start competing with your critical connections. Only origins you actually use.
<!-- The classic Google Fonts pattern -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- googleapis = CSS (same-origin fetch), gstatic = font (crossorigin) -->For how fonts actually load and why FOUT / FOIT happen, see the how-web-fonts-load guide. The internals of that connection cost live in how-cdns-actually-work.
preload — what this page needs, at high priority
<link rel="preload" href="/fonts/inter.woff2" as="font"
type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image"
fetchpriority="high">
<link rel="preload" href="/critical.css" as="style">- Fetches a resource this page needs soon, right now, at high priority. Unlike dns-prefetch / preconnect, which warm a connection, preload downloads the actual resource.
- as is mandatory —
as="font"/as="image"/as="style"/as="script". Withoutasthe browser can't decide the priority, CORS mode, or Accept header, so the hint is ignored or the resource is fetched twice. - Fonts need crossorigin — fonts are fetched in CORS mode even same-origin, so missing
crossorigincauses a double-fetch. - When — late-discovered critical resources: a font referenced from CSS, the LCP image, a critical chunk imported dynamically by JS. Resources already visible in the initial HTML usually don't need preload.
- Unused warning — if you preload something and don't use it within a couple of seconds, the console logs "was preloaded but not used." That's a signal you just wasted bandwidth.
fetchpriority — priority hints
<img src="hero.avif" fetchpriority="high"> <!-- LCP image -->
<img src="below-fold.jpg" fetchpriority="low"> <!-- off-screen -->
<script src="analytics.js" fetchpriority="low"></script>high/low/auto— nudge the browser's default priority up or down. Works onimg,script,link, and fetch().- By default the browser starts the first image at low priority (it doesn't know yet whether it's the LCP). Giving the LCP image
fetchpriority="high"promotes it immediately — one of the highest-leverage single changes you can make. - Conversely, drop off-screen images and non-essential scripts to
lowso they yield bandwidth to critical resources. - Combine with preload —
<link rel="preload" as="image" fetchpriority="high">handles discovery and priority in one line.
prefetch — low-priority cache for the next navigation
<link rel="prefetch" href="/dashboard" as="document">
<link rel="prefetch" href="/next-page.js" as="script">- Fetches a page or resource you'll likely need next at the lowest priority and stores it in cache. Done while the current page is idle.
- Difference from preload — preload is this page, now; prefetch is the next page, later. Opposite priority and opposite timing.
- When — a pagination "next", the next step of a wizard, a detail page on hover: cases where the user's next move is nearly certain.
- If you guess wrong, the fetched bytes are thrown away. Use it only on confident paths.
modulepreload — fetch an ES module AND parse it
<link rel="modulepreload" href="/app.js">
<link rel="modulepreload" href="/router.js">
<link rel="modulepreload" href="/store.js">- A preload specialized for ES modules. It doesn't just download — it parses, registers in the module map, and resolves the dependency graph ahead of time.
as="script"is implied, so you don't write it. Unlike a plainpreload as="script", it walks the import graph downward.- When — entry modules with a deep import chain. Bundlers (Vite and friends) often inject it automatically for dynamic-import chunks.
103 Early Hints — hints before the response is ready
-- Server flow
1. Request arrives → server begins rendering the page (DB queries, slow)
2. Before the body is ready, it sends first:
HTTP/1.1 103 Early Hints
Link: </app.css>; rel=preload; as=style
Link: <https://cdn.example.com>; rel=preconnect
3. Body is ready:
HTTP/1.1 200 OK
...the actual HTML...- While the server is still building the final 200 (rendering, waiting on the DB), it sends a
103with just the preconnect / preload hints. The browser spends that time warming connections and starting downloads. - It beats
<link>tags inside the HTML because it doesn't wait for the first byte of HTML (TTFB). - Supported by CDNs like Cloudflare and Fastly and by Chrome. Especially effective for pages with slow server-side rendering.
Speculation Rules API — prefetch or prerender the next page
<script type="speculationrules">
{
"prefetch": [
{ "where": { "href_matches": "/articles/*" }, "eagerness": "moderate" }
],
"prerender": [
{ "where": { "selector_matches": "a.next" }, "eagerness": "eager" }
]
}
</script>- A JSON ruleset tells the browser "these links are OK to prefetch / prerender." prerender goes further and actually renders the next page in the background, so a click shows it instantly.
- It's the modern replacement for the deprecated
<link rel="prerender">. You get URL-pattern matching, eagerness levels (conservative / moderate / eager), and hover / pointerdown triggers for fine control. - Prerender is expensive (it renders a whole page). Lower the eagerness to reduce mis-fires, and exclude pages with side effects like analytics or checkout.
Common mistakes
1. Over-preloading
"Faster is better" so you preload everything — and now everything is high priority, which means nothing is prioritized. The truly critical LCP image and font end up fighting the next-line non-essential resources for bandwidth. Keep preload a short, hand-picked list.
2. Missing as / crossorigin
<!-- Bad — no as → ignored or double-fetched -->
<link rel="preload" href="/inter.woff2">
<!-- Bad — it's a font but no crossorigin → connection not reused -->
<link rel="preload" href="/inter.woff2" as="font">
<!-- Good -->
<link rel="preload" href="/inter.woff2" as="font"
type="font/woff2" crossorigin>3. The unused-preload warning
Preloaded but not used within a few seconds → "was preloaded but not used." The usual cause is a mistyped URL (a query string, hash, or version that differs from the URL actually used) or a resource you no longer use. If the URL differs by even one byte it's treated as separate — hello, double-fetch.
4. Preconnecting everywhere
Preconnect to an origin you don't end up using and you've opened a connection for nothing. A TLS handshake isn't free. Reserve it for the 2-4 critical origins the first screen genuinely needs.
5. Bad prefetch guesses
Prefetch a page the user never visits and it's pure waste — especially rude on data-saver mode and slow connections. The conservative eagerness in Speculation Rules is the safe default.
Which hint, when — summary table
hint fetches priority when
──────────────────────────────────────────────────────────────
dns-prefetch DNS only - less-critical 3rd-party
preconnect DNS+TCP+TLS - critical origin you'll use
preload resource (this page) high late-discovered font/LCP/CSS
modulepreload ES module + graph high deep import-chain entry
prefetch resource (next page) lowest near-certain next navigation
fetchpriority (attribute, no new fetch) LCP=high, off-screen=low
103 Early Hints server-sent preconnect/preload slow-TTFB SSR pages
Speculation next-page prefetch/prerender SPA-style navigation guessThe Core Web Vitals tie-in
The real-world payoff of resource hints is mostly measured in LCP and CLS. Promote the LCP image with preload + fetchpriority="high" and LCP drops directly; fetch fonts early with preconnect / preload and you shrink both the layout shift (CLS) from a late font swap and the window of invisible text. But adding hints without measuring can backfire — always confirm before/after with Lighthouse or WebPageTest. The metrics themselves are covered in the how-core-web-vitals-work guide.
References
- MDN — Preloading content with rel=preload — developer.mozilla.org
- web.dev — Optimize resource loading with the Fetch Priority API — web.dev/fetch-priority
- Chrome — Speculation Rules API — developer.chrome.com
- RFC 8297 — 103 Early Hints — datatracker.ietf.org
Summary
- Resource hints move resource discovery earlier to shorten the critical chain.
- dns-prefetch (DNS) < preconnect (DNS+TCP+TLS) < preload (the actual resource) — each warms the path a little deeper.
- preload requires
as; fonts requirecrossorigin. Otherwise it's ignored or double-fetched. - Put fetchpriority="high" on the LCP image — the highest-leverage single change.
- prefetch / Speculation Rules are for the next navigation. preload is for this page.
- 103 Early Hints warms connections before the response, ideal for slow-TTFB SSR.
- The most common mistakes are over-preloading and missing as/crossorigin. Few, precise, and measured.