DeliveryGENERALSPEC-EVOLVINGNETWORK-SPECIFIC

Resource Hints

preconnect, dns-prefetch, preload, modulepreload and prefetch are five different jobs — and a hint that guesses wrong costs more than no hint at all.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

How do I tell the browser about a resource before it would have discovered it, and when is doing so a net loss?

The user intent

A person is going to need something the browser does not know about yet — a font hidden inside a stylesheet, an origin the page will contact in a moment, the code for the page they are about to click through to.

The obvious build

Hints make things faster. Add preload to the important resources and prefetch to the likely next pages, and the browser gets a head start.

Why it breaks

A preload with the wrong as value fetches the resource, fails to match it against the eventual request, and downloads it a second time. You paid twice for one file and delayed everything else on the way.

How it breaks in a real browser
  • A preload with the wrong as value fetches the resource, fails to match it against the eventual request, and downloads it a second time. You paid twice for one file and delayed everything else on the way.
  • A preload for a resource the page never uses produces a console warning nobody reads and a download nobody needed, competing for bandwidth with the resources that were on the critical path.
  • Ten preloads in the head do not make ten resources fast. They make the browser's priority assignment meaningless, because everything cannot be first.
  • preconnect to eight origins opens eight connections, each with a handshake, most of which are closed unused before anything is requested through them.
  • prefetch for a route the user does not visit is bandwidth spent on a guess — and on a metered connection it is the user's money spent on your guess.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • A hint changes when and at what priority the browser does something it would have done anyway, or would have done later. It never changes what the resource is or how it is processed.
  • dns-prefetch resolves a hostname ahead of time. Cheap, low-risk, and only saves the resolution step — which is sometimes the whole win on a cold mobile connection (Following One Lookup Through Every Cache in Networking).
  • preconnect goes further: resolve, connect, and negotiate TLS, so the first request to that origin starts with a warm connection. Expensive enough that a handful is a budget, not a starting point.
  • preload fetches a specific resource now, at high priority, with an as that declares what kind of thing it is. The as is not decoration: it sets the priority, the Accept header, and the cache key the eventual request must match.
  • modulepreload is preload for JavaScript modules, and it does more — it fetches the module *and* its dependency graph, and prepares it for evaluation rather than merely putting bytes in the cache (ESM vs CommonJS).
  • prefetch is speculative and low priority: fetch this for a *future* navigation, when the browser has nothing better to do. It is the only one of the five that is a bet on what the user does next.
  • Priority is zero-sum. A connection has a finite amount of throughput in flight, and raising one resource's priority necessarily lowers everything else's (Congestion Control: Protecting the Network in Networking).

What this makes the browser do

And which of it is avoidable.

  • Resolving hostnames and opening connections that may never be used, each holding a socket and a TLS session for a while.
  • Fetching preloaded bytes into the memory cache and holding them there, unmatched, until a real request claims them or a timeout drops them.
  • Re-prioritising in-flight requests as the document parses and as hints arrive, which is real scheduling work with real consequences for what lands first.
  • For modulepreload, walking and fetching the module graph, and compiling ahead of evaluation — main-thread work moved earlier rather than removed (The Module Graph).
  • For prefetch, storing a response that may be evicted before the navigation that would have used it ever happens.

Five hints, five different jobs

They are usually taught as a list of similar things, which is how people end up using the wrong one. They are not similar. Two are about a connection, two are about this page, and one is about the next page.

The right way to read the table: find the column headed "what it costs when wrong", because that is the column that decides whether a hint belongs in your head at all.

HintWhat it doesUse it whenWhat it costs when wrong
dns-prefetchResolves a hostname earlyYou will probably contact an origin, and want the cheapest possible head startAlmost nothing — a resolution you did not need. The safe default for uncertain origins.
preconnectResolves, connects and negotiates TLSYou are *certain* an origin will be used early — an API host, a font host, an image CDNA connection and a handshake, held open then dropped unused. Budget a couple, not a dozen.
preloadFetches a specific resource now, at high priorityA path-critical resource the parser cannot discover: a font named inside CSS, an image named inside a stylesheetA wasted download at the *highest* priority, delaying the resources that were actually critical. The most expensive mistake on this list.
modulepreloadFetches a module and its dependency graph, and prepares it for evaluationThe entry module of a page whose graph is deep enough that discovery costs several sequential hopsThe same as preload, plus a graph walk. Pointing it at the wrong entry fetches an entire subtree nobody wanted (Code Splitting).
prefetchFetches at idle priority for a *future* navigationThe next step is highly predictable — a wizard, a paginated list, a link the user is hoveringBandwidth spent on a guess, on a connection that may be metered. The only hint with an ethical dimension.
The details that decide whether a hint works at all
1<!-- Fonts are fetched in CORS mode even from your own origin.
2 Omit crossorigin and the preload never matches the request the
3 stylesheet makes, so the font downloads twice. -->
4<link rel="preload" href="/f/inter.woff2" as="font"
5 type="font/woff2" crossorigin>
6
7<!-- `as` sets priority, the Accept header, and the cache match.
8 `as="script"` for a stylesheet is not a typo the browser
9 forgives; it is a different request. -->
10<link rel="preload" href="/hero.avif" as="image"
11 type="image/avif" fetchpriority="high">
12
13<!-- Certain, early, third-party: worth a full handshake. -->
14<link rel="preconnect" href="https://api.example.com" crossorigin>
15
16<!-- Probable but not certain: cheap insurance instead. -->
17<link rel="dns-prefetch" href="https://images.example.com">
18
19<!-- The module graph, prepared rather than merely downloaded. -->
20<link rel="modulepreload" href="/app.a3f19c.js">

Three of these five lines fail silently when they are wrong: no error, no exception, just a resource fetched twice or not matched. The console's "preloaded but not used" warning is the only automatic feedback you get, which is why it is worth treating as an error in review.

Choosing one — or none

The decision is not "which hint", it is "is a hint the right tool at all". Most late-starting resources are late because of the page's structure, and restructuring is more durable than annotating.

A hint is a second source of truth about your page's dependencies. The first source is the markup and the build output; the hint duplicates part of it in a place that no build step verifies. That duplication is the real cost, and it is why the first option below is usually the right one.

A resource starts later than it should. What now?

What is the most durable way to make this resource arrive earlier?

Remove the hop instead

when The resource is discoverable in principle — a font you could reference from the document, an image you could put in markup, a chunk you could import statically

cost Restructuring, sometimes across a build boundary. Buys the largest and most durable win, because the parser and the preload scanner do the work for free from then on (The Preload Scanner).

`preload` it

when The resource is genuinely undiscoverable — named inside CSS, or fetched by application code — and it is on the critical path

cost A hardcoded URL that must be regenerated whenever the asset name changes, plus exact as and crossorigin matching. Fails quietly when either drifts.

`preconnect` the origin

when You cannot know the URL ahead of time but you know the host, and the first request to it is early and important

cost A handshake you may not use. Strictly limited: each one competes with the others, and unused connections are closed after a short window.

`prefetch` the next step

when The next navigation is predictable and the connection is not metered — ideally triggered by hover or viewport entry rather than by page load

cost Bandwidth for users who do not take that step. Gate it on user intent, and consider gating it on the connection information the browser exposes.

Do nothing

when The resource is not on the critical path, or the browser's own priority for it is correct

cost Nothing — and this is the correct answer far more often than the number of hints in a typical head suggests.

Preloading everything is preloading nothing

Priority is a ranking, and a ranking in which everything is first is not a ranking. A head with a dozen preloads has told the browser that a dozen things are urgent, which the browser resolves by falling back to its own ordering while still having paid for the extra contention.

The compounding version is worse. Hints accumulate across teams and quarters: each was added by someone with a real observation, none are removed when the observation stops being true, and after two years the head is a fossil record of past waterfalls competing with the current one.

Treat a hint as code with an owner and an expiry. Every one should be traceable to a waterfall that showed a late start, and should be re-checked when the resource it names moves — which, for a content-hashed asset, is every deploy (Content-Hashed Assets).

What a wrong hint actually does
TriggerSymptomCauseResponse
preload with an as that does not matchTwo identical rows in the Network panel for one URLThe cache entry from the preload does not match the eventual request's type, so it cannot be reusedFix the as. Every preload should be verified by loading the page once and counting the rows.
Font preloaded without crossoriginFont downloads twice; the swap still happensFonts are requested in CORS mode; the non-CORS preload is a different cache entryAdd crossorigin. This one is worth a lint rule because it is invisible and near-universal.
A dozen preloads in the headFirst paint gets *later* after adding hintsThe stylesheet on the critical path now competes with everything that was declared equally urgentDelete every hint that cannot be traced to a specific late start, then re-measure (Reading a Network Waterfall).
Preload URL hardcoded in a templateA 404 on every page load after a deploy, plus a wasted requestThe asset is content-hashed and the hint was not regeneratedGenerate hints from the build manifest, never by hand (Deploying a Frontend).
prefetch fired on page load for every linkLarge background data usage; angry reports from users on metered plansSpeculation with no signal of intent behind itTrigger on hover, focus or viewport entry, and respect the user's data-saving preference.
preconnect to many originsConnections opened and closed without carrying a requestEach preconnect is a full handshake; unused ones are dropped after a short windowKeep the certain ones, downgrade the rest to dns-prefetch.

How to build it

Most important first.

  • Start from the waterfall, not from a list of hints. A hint is a fix for a specific late start you have already observed (Reading a Network Waterfall).
  • Prefer removing the hop to hinting around it. If a font is discovered inside a stylesheet, referencing it in the document is better than preloading it, because it is one fewer thing to keep in sync.
  • Use preconnect for the one or two third-party origins you are certain will be used early. Anything you are not certain about gets dns-prefetch, which costs almost nothing when wrong.
  • Use preload for path-critical resources the parser genuinely cannot see: a font referenced from CSS, a hero image named in a stylesheet, a script fetched by other script. Match as and crossorigin exactly.
  • Treat prefetch as a product decision, not a technical one. Prefetch on intent — a hover, a viewport entry, a link the user is clearly heading for — rather than on page load (Lazy Loading).
  • Budget them. Write down how many hints the head is allowed to have and enforce it, because hints accumulate: every one was added by someone who had a reason, and nobody removes the old ones.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Hints have no direct accessibility surface, which is exactly why the indirect ones need stating: they change what arrives when, and therefore what a screen-reader user encounters at each moment of the load.
  • Preloading a font that the first screen's text needs prevents a swap, and a swap reflows text a magnifier user may be mid-sentence in (Images and Fonts).
  • Prefetching a next page can make a navigation feel instant, which is a genuine benefit for users with motor impairments for whom every extra wait compounds. It also means the announcement of the new page arrives faster than a screen reader may expect after the activation (Client-Side Routing).
  • Over-hinting slows the resources that carry the page's actual content, so a head full of speculative hints makes the document reachable later for everybody, including people whose only route into it is that document.

What can go wrong

Failure modes
  • The unused preload. Console says the resource was preloaded and not used within a few seconds; the file was downloaded, cached, and evicted without ever being needed.
  • The mismatched preload. A font preloaded without crossorigin never matches the CSS-initiated request, so it is fetched twice — the classic version of this bug and still the most common.
  • The stale preload. A build renames a chunk, the hardcoded hint in the template still points at the old name, and every page load requests a file that no longer exists (Content-Hashed Assets).
  • The hint that outlived its reason. Someone preloaded a script that has since moved into the main bundle; the hint stays, competing with the bundle for the same bandwidth.
  • Prefetching an entire application. On a metered or slow connection this is actively hostile, and the user has no way to see it happening or stop it.
What can arrive out of order
  • A preloaded resource and the request that was supposed to consume it race: if the attributes do not match, the second request does not find the first in the cache and both complete.
  • A prefetched next-page resource races the navigation itself. If the user clicks before the prefetch lands, the prefetch is at best neutral and at worst competing with the navigation.
  • Multiple preconnects race for the same limited connection budget, and the browser may close the ones it has not used yet, in an order you cannot control.
Security
  • preconnect and dns-prefetch leak intent: they tell an origin — and every resolver and observer on the path — that this user is on a page that expects to contact it, before any request is made.
  • prefetch fetches a URL with the user's cookies for that origin, so prefetching an authenticated page can have side effects on the server if that page is not safe to GET (GET: The Promise of Safety in API Design).
  • A preload for a cross-origin resource without the correct crossorigin mode is not just a cache miss, it is a different request with different credentials semantics.
  • A Content-Security-Policy applies to hinted requests exactly as it does to real ones; a hint for an origin the policy forbids is blocked, which is confusing to debug because nothing visible was supposed to happen yet (Content Security Policy).
Misreads
  • "preload makes it load faster." It makes it load *earlier*, at the expense of something else. Total bandwidth did not change.
  • "prefetch and preload are the same with different scopes." They are opposites in priority: preload is urgent and for this page; prefetch is idle-time and for a possible next page.
  • "More hints, more speed." Beyond a handful, hints degrade the browser's own prioritisation, which was informed by the actual document.
  • "preconnect is free." It costs a connection and a handshake on both ends, and unused ones are dropped after a short window, so a wrong guess is pure waste.
  • "The browser needs my help." It usually does not. Hints are for the things the browser cannot see — resources referenced from inside other resources (The Preload Scanner).

Measuring it, and what changes in the field

How you would see this
  • The console warns about a preloaded resource that was not used. That warning is the single highest-value signal in this lesson and it is routinely ignored.
  • The Network panel shows whether a preloaded resource was fetched once or twice — a duplicate row for the same URL is a mismatch, almost always as or crossorigin (Debugging the Network).
  • Compare priority columns before and after. If adding a hint pushed the stylesheet down the list, you have moved the problem rather than fixed it.
  • Measure the resource you hinted *and* the resources you did not. Hints are zero-sum, so a change that only reports on its own target is not a measurement (Measure Before Optimising).
Slow device, slow network, large data, old tab
  • On a high-latency connection preconnect is worth the most, because it removes round trips rather than bytes.
  • On a low-bandwidth or metered connection prefetch is worth the least and costs the most, and should probably be gated on a connection check.
  • On HTTP/1.1 hints interact with the per-origin connection limit, so a preload can starve a resource you needed more; on HTTP/2 and HTTP/3 the effect is spread across the multiplexed connection instead (HTTP/2: Streams on One Connection in Networking).
  • On a repeat visit the hinted resource is likely already cached, so most hints do nothing and the ones that still open connections still cost something.
What this costs
  • Every hint is a hardcoded assumption about the page that must be maintained. When it goes stale, it fails quietly and expensively.
  • Hints in HTML must be generated, because path-critical assets have content hashes. That couples your document template to your build output (Deploying a Frontend).
  • Raising a resource's priority lowers everything else's. There is no version of this where you get the benefit without paying somewhere.
  • Prefetching improves the next navigation for the users who take it and wastes bandwidth for the ones who do not, and you generally cannot tell which group a given user is in.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe five hints are specified and broadly supported, and the semantics — what each one does to discovery and priority — are consistent. What differs is how each engine maps a hint onto its internal priority levels, so the same hint can reorder a waterfall differently in Chromium and in Safari.
  • SPEC-EVOLVINGThis area moves: priority hints on elements, speculation rules for prefetching and prerendering whole documents, and early hints delivered before the response body all overlap with the five here, and support is uneven. Learn what the primitives do to discovery and priority; check current support before shipping any specific one.
  • NETWORK-SPECIFICHints are worth the most where round trips are expensive and the least on a warm, low-latency connection. On HTTP/1.1 they compete for a small connection pool and can starve the critical path; on multiplexed HTTP/2 and HTTP/3 they redistribute priority within one connection instead, which is a smaller and less dangerous effect.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.