What you'll learn: By the end of this lesson you will be able to say which of four questions a loading hint answers (when is the resource discovered, how urgent is it, is it for this page or the next, is the connection ready), use
preload,modulepreload,prefetch,preconnect,fetchpriorityand Speculation Rules for the right job, avoid each one's failure mode (a missingas, a mismatchedcrossorigin, prefetching a URL with side effects), and choose a hint with a short procedure.
In a restaurant kitchen the cook starts a dish when the order reaches the pass. A good waiter helps in three ways. They call ahead: "table four will need the fish soon". They say: "table five will probably order dessert, start the base in a quiet moment". And they say: "do the soup first, that guest is waiting". A waiter who calls out everything makes the day worse, because the cook cannot do everything at once.
A browser is that cook. It discovers resources as it reads the page and has a limited number of bytes per second to spend. Resource hints are the waiter: a line of HTML, an HTTP header or some JSON that tells the browser what it could not yet know. The running example is an online store's product page: its hero image is set from CSS, a brand font is named in a stylesheet, a reviews panel loads a JavaScript chunk when a tab is clicked, and most visitors who leave go to the checkout. Optimizing Largest Contentful Paint and Images and Fonts show a few hints in context; here they are the subject.
The core idea
Every hint answers one of four questions. Mixing them up causes most hint bugs.
- Discovery. Will the browser find the resource early enough? A resource named only inside a stylesheet, or created by script, is found late.
preloadandmodulepreloadsay "this exists, start now". - Priority. Of the things being fetched, which goes first?
fetchpriorityraises or lowers the browser's default. - Scope. Is it for this page or a future navigation?
preloadis for the current page.prefetchand Speculation Rules are for the next one. - Connection. Is the server ready to talk?
preconnectanddns-prefetchdo the handshake early.
A hint never makes the network faster. It changes when work starts or in what order, and the page's bytes are shared, so a hint that helps one resource costs the others a little.
Figure 1. Four hints, four questions
How to read this: the top row is the current page. Preload moves discovery earlier, fetchpriority reorders the queue and preconnect warms a connection. The bottom row is the next navigation, served from a cache that a low-priority fetch filled earlier.
The hint family, as the HTML Standard defines it
The HTML Standard defines each hint as a link type. Its wording separates must (an instruction) from should (advice the browser may ignore).
| Hint | What the standard says | Scope | Strength |
|---|---|---|---|
dns-prefetch |
Resolve the origin's DNS name early | this page | hint |
preconnect |
Connect to the origin early; "should attempt" a full handshake, may do part or skip it | this page | hint |
preload |
Fetch and cache for the current navigation, using the destination in as |
this page | must |
modulepreload |
Fetch a module script into the document's module map; dependencies optionally | this page | must |
prefetch |
Fetch and cache a resource likely needed for a followup navigation; the browser may delay it to favour what the current document needs | next page | should |
Also from the standard: preload and preconnect links must not delay the load event, and Link header processing (the hint sent as an HTTP header) is defined for those two but does nothing for prefetch. How urgent each request is, is left to the browser; where this lesson names priorities it uses Chromium's documented values.
Preload: say it early, say it once
MDN describes rel="preload" as declaring fetch requests in the <head> for resources the page "will need very soon", so they start "before browsers' main rendering machinery kicks in". It does not run a script or apply a stylesheet; it schedules a download and caches the result.
<link rel="preload" href="/fonts/brand-regular.woff2" as="font" type="font/woff2" crossorigin>
asnames the kind of resource. MDN listsfetch,font,image,script,styleandtrack, and says it lets the browser pick the right cache, content security policy andAcceptheader. In the standard's steps a link whoseasis not a valid preload destination makes no request, so a missingasis a silent no-op.typelets the browser skip a format it does not support.mediamakes a preload conditional (media="(width <= 600px)").crossoriginmust match how the resource will later be requested. MDN saysfontandfetchpreloads need it "even when the fetch is not cross-origin", because they are fetched in anonymous CORS mode.
Illustration 1: the minimal case. The product page's hero is a CSS background-image, found only after the stylesheet has been downloaded and applied. A preload moves discovery to the first bytes of HTML:
<link rel="preload" href="/img/kettle-hero-1200.avif" as="image" type="image/avif" fetchpriority="high">
Responsive images use imagesrcset and imagesizes; those and 103 Early Hints are in the LCP lesson.
What preload is not. Web.dev says "Preload is a mandatory fetch, not a hint", and it still fetches at the resource's default priority unless told otherwise. An unused preload spends bandwidth for nothing. After adding one, open the Network panel and check that the resource is requested once.
For ES modules use <link rel="modulepreload" href="/assets/reviews-4f2a.js">. MDN says it also parses and compiles the module into the module map. Fetching its imports is allowed but not required ("a browser-specific optimization"), so the portable approach is to list each dependency. Code splitting covers bundlers that emit these links. MDN marks it Baseline widely available since September 2023.
Preconnect and dns-prefetch
preconnect starts DNS, TCP and (for HTTPS) TLS for another origin. MDN says it "has no benefit on same-origin requests because the connection is already open", is "best used for only the most critical connections", and that preconnecting to many third-party domains "can be counterproductive". dns-prefetch does only the DNS step, a cheaper hint for origins that might be used. Reserve preconnect for one or two origins on the critical path, such as the image CDN that serves the hero.
Priority: fetchpriority reorders the queue
Browsers assign each request a priority from its type and position. Web.dev's page on Fetch Priority says fetchpriority "sets relative priority", raising or lowering the default. It takes high, low and auto, works on img, link and script, has a priority option on fetch(), and is "a hint, not a directive". Support, per web.dev: Chrome and Edge 102, Firefox 132, Safari 17.2.
Chromium defaults (web.dev, at the time of writing). These are one engine's choices, not the standard's.
| Resource | Default priority |
|---|---|
| Main HTML, early CSS in the head | VeryHigh |
| Font | VeryHigh; a preloaded font starts at High |
| Early script | High |
async or defer script |
Low |
| Image in the viewport after layout | Starts Low, raised to High |
| First five large images (since Chrome 117) | Medium |
fetch() and async XHR |
High |
prefetch |
VeryLow |
The lesson in the table is that priority is zero-sum: if five things are High, none stands out. Good uses are narrow. Mark one or two first-screen images high so they start at High instead of waiting for layout. Mark off-screen carousel images, and a preload of a non-critical script, low. Use priority: "low" for a background fetch(). Raise an async script that matters more than its default of Low.
Web.dev says preload and priority do different jobs: preload helps discovery but not urgency, and fetchpriority helps urgency but not discovery. A preloaded hero usually needs both. It adds that gains are larger on constrained connections.
Prefetch: spend idle time on the next page
prefetch is for a future navigation. MDN says it is like a low-priority fetch(), generally lower than preload because "the current page is more important than the next". The result is kept in the HTTP cache, so it works for subresources the current page never uses, such as the checkout bundle. Limits: a Cache-Control: no-store or no-cache response may not be usable from a prefetch; many browsers partition the cache per top-level site, which MDN says makes prefetch useless for resources used by a different site; and the request carries Sec-Purpose: prefetch, which a server can act on. Because of this MDN advises Speculation Rules for document prefetches where supported, and rel=prefetch for subresources.
Speculation Rules: prefetch or prerender a page
The Speculation Rules API targets document URLs, so it suits multi-page sites more than single-page apps. A script of type speculationrules holds JSON:
<script type="speculationrules">
{
"prefetch": [
{
"where": {
"and": [
{ "href_matches": "/product/*" },
{ "not": { "selector_matches": ".no-prefetch" } }
]
},
"eagerness": "moderate"
}
],
"prerender": [
{ "urls": ["/checkout"], "eagerness": "conservative" }
]
}
</script>
Prefetch versus prerender. Per MDN, a prefetch downloads the page's response body and none of its subresources, at a cost "much smaller" than a prerender. A prerender loads the page in an invisible tab and runs its JavaScript, at "about the same amount of resources as rendering an <iframe>", and the later navigation is near-instant. MDN encourages prefetching broadly and prerendering "more sparingly".
Eagerness says how early to act (MDN): immediate as soon as possible; eager on "any slight suggestion", such as a hover; moderate after reasonable evidence, such as hovering a link in view; conservative only when the user starts to click. Unset, list rules (urls) default to immediate and document rules (where) to conservative. The browser may weigh the hint against its own heuristics.
Figure 2. A prefetch triggered by a hover
How to read this: the request is real and reaches the server. Only the reuse is speculative, so the server pays for every wrong guess.
Guardrails (Chrome's documentation, at the time of writing).
| Chrome limit | Prefetch | Prerender |
|---|---|---|
immediate |
50 | 10 |
eager or moderate, desktop |
2 | 2 |
eager or moderate, mobile |
6 | 2 |
conservative |
2 | 2 |
Chrome skips speculation when Save-Data is on, under battery saver with a low battery, under memory constraints, or in a background tab. Prerender is same-origin by default, and cross-site prefetch works only when the user has no cookies for the destination site. A response outside 200 to 299 is not used, and Chrome keeps prefetched pages for about five minutes.
Support is partial. MDN marks the API experimental and not Baseline; Chrome's page shows Firefox as not supported for several features and Safari as behind a flag or not supported. Treat it as progressive enhancement: feature-detect with HTMLScriptElement.supports("speculationrules"), and keep the page correct without it.
The safety rule: a speculated URL runs on your server. MDN lists URLs that must not be speculated: sign-out, language switching, add to cart, sending a one-time password, incrementing a usage allowance, recording an ad conversion. A prerendered page also runs its script, so an analytics tag can record a visit nobody made; the Document.prerendering property lets scripts wait for activation. Make state-changing endpoints POST, exclude them with a not rule, and never treat Sec-Purpose as authentication.
Choosing: a short procedure
Figure 3. Which hint for which resource
or the next?"} -->|"this page"| Q2{"Can the browser find it
in the HTML?"} Q1 -->|"next page"| Q5{"Whole document
or a subresource?"} Q2 -->|"yes"| P1["Leave it. Adjust fetchpriority
if the default is wrong"] Q2 -->|"no: CSS, font, script-made"| Q3{"A module
chunk?"} Q3 -->|"yes"| M["modulepreload"] Q3 -->|"no"| P2["preload with as, type,
crossorigin"] P1 --> Q4{"Different origin?"} P2 --> Q4 M --> Q4 Q4 -->|"yes"| C["preconnect (one or two)"] Q5 -->|"document"| S["Speculation rules:
prefetch, prerender sparingly"] Q5 -->|"subresource"| F["rel=prefetch"]
How to read this: the cheapest correct answer is often "put it in the HTML and do nothing else". A hint is for resources the browser cannot find, or finds in the wrong order.
| Hint | Helps | Costs | Typical failure |
|---|---|---|---|
preload |
Late-discovered resources of this page | Bandwidth if unused; competes with others | Missing as, wrong crossorigin, too many |
modulepreload |
Route chunks and imports | Download, parse and compile work | Listing every module on every page |
preconnect |
First request to a critical other origin | A connection that may go unused | Preconnecting to many origins |
fetchpriority |
Order of a few key requests | Others wait | Marking many things high |
prefetch (link) |
Subresources of the likely next page | Idle bandwidth, cache space | Responses with no-store |
| Speculation rules | Near-instant next navigation | Server load, memory, side effects | Speculating state-changing URLs |
Illustration 2: the realistic example. On the product page the team keeps the hero as a server-rendered <img> with fetchpriority="high" (no preload needed), preloads the one brand font used above the fold, preconnects to the image CDN, uses modulepreload for the reviews chunk on the product route only, and adds a moderate document prefetch for /product/* and a conservative prerender for /checkout. Nothing is hinted that the browser already finds.
Illustration 3: the failure case. A template adds rel="preload" for six fonts, four images and two scripts "to be safe". Each is a mandatory fetch, so the one image that is the LCP competes with eleven others for the same bandwidth. Two images belong to a tab nobody opens first, and a font preload lacks crossorigin, so it does not match the anonymous-CORS request the font rule makes, the mismatch MDN warns about. LCP is worse than before. The fix is to remove hints until the Network panel shows only what the first screen needs.
In practice: a store that prefetched everything
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company, and every number is an illustrative assumption.
The situation
An online store's product pages are server-rendered. Product managers want category pages to feel instant. A developer adds one document rule: every same-origin link on a category page is prefetched, eager. Lab navigation times drop and the change ships on a Friday.
Starting architecture
Figure 4. What the broad rule matches
How to read this: the rule does not know which links are safe. The cart link is an ordinary anchor, so it matches, and prefetching it is a real request that changes server state.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
| Keep the broad rule, add an exclusion list | Keeps the speed-up | A list to maintain | A new unsafe link slips through |
Allow-list /product/* at moderate |
Fewer wasted requests, nothing unsafe | A narrower speed-up | Misses links outside the pattern |
Make state changes POST and keep the rule |
Fixes the class of bug | A front-end and back-end change | Needs a migration |
The decision
| Field | Decision record |
|---|---|
| Context | A broad rule prefetches every link, including ones that change state. |
| Decision | Allow-list /product/* and /category/* at moderate, exclude .no-prefetch, and make "add to cart" a POST form button. |
| Consequences, good | Prefetches are safe to repeat; the server sees fewer speculative requests. |
| Consequences, bad | Two teams ship together; unlisted pages get no speed-up. |
| Revisit when | Field data shows prefetched pages are rarely activated. |
Rollout and verification
- Log the header. Count requests with
Sec-Purpose: prefetchper route against actual navigations to them. - Ship the allow-list to a small share of traffic and compare field LCP for navigations that used a prefetch against those that did not.
- Replay a prefetched sequence in staging and diff cart and database state, and watch analytics for page views with no click.
The 2 a.m. failure
Support reports carts holding items nobody added, and stock counts dropping for products no order has bought. The "add to cart" link was a GET, and the eager rule fetched it for every link in view. First three checks: (1) filter access logs for Sec-Purpose: prefetch on /cart/add; (2) compare cart changes with and without that header; (3) read the rule's matching pattern in the page source. Mitigation: exclude the path with a not rule and deploy; remove the added items by session. Follow-up: make the endpoint a POST and add a test that fails when a link matched by a speculation rule is not safe to call twice.
Architect's notes
- Start from the resource inventory. A list of what the first screen needs, in order, nearly writes the hint list. Hints without an inventory are guesses.
- Prefer discoverable over hinted. Server-render the hero
<img>and the stylesheet link; preload only what cannot be found. - Treat speculation as a capacity decision. Every wrong guess is a real request. Cap it with
moderate,conservativeand an allow-list. Defer prerender until the page is safe to render without a user. - Put hints in templates with an owner and a test. They rot when layouts change.
Common mistakes
- Preloading without
as, so nothing is fetched, or a font without a matchingcrossorigin. - Putting
fetchpriority="high"on many images. - Using
rel="prefetch"for a document and expecting it to help a cross-site navigation. - Speculating a URL that changes state, or running analytics in a prerendered page.
Key takeaways
- A loading hint answers one of four questions: discovery, priority, scope or connection.
preloadandmodulepreloadare mandatory fetches for this page;prefetch,preconnectanddns-prefetchare hints the browser may ignore.- Preload needs the right
as,typeandcrossorigin; without a validasnothing is fetched. fetchpriorityis relative and zero-sum: use it on one or two resources.- For the next page prefer Speculation Rules (prefetch broadly, prerender sparingly) and keep
rel=prefetchfor subresources. - A speculated URL is a real request: make state changes
POST, exclude them, and handle analytics in prerendered pages. - Support differs by browser; feature-detect and keep the page correct without the hint.
Practice exercises
Exercise 1 (30 min): In DevTools, open the Network panel's Priority column for a page of your choice and list five resources with their priorities. Which differ from what you expected?
Exercise 2 (1 hour): Build a page whose hero is a CSS background-image. Note when its request starts, add a preload with as="image", and compare. What changed and what did not?
Exercise 4 (half day): Add a moderate document prefetch to a small multi-page site. Log Sec-Purpose on the server and count prefetches never followed by a navigation. Then write a check that fails when a rule matches a URL not on a safe-to-repeat list, and say what it cannot see.
Related reading
In this course:
- Optimizing Largest Contentful Paint: the LCP case for discovery and
fetchpriority,imagesrcsetand Early Hints. - Images and Fonts: preloading the critical font and sizing images.
- Code Splitting, Lazy Loading and Tree Shaking: the chunks that
modulepreloadandprefetchtarget. - The Critical Rendering Path: what blocks first paint and why discovery order matters.
- HTTP Caching for the Front End: why
no-storeblocks reuse. - Loading Performance and Core Web Vitals: measuring the effect in the field.
Official documentation
- HTML Standard: link types:
preload,prefetch,preconnect,dns-prefetchandmodulepreload(read 2026-11-08). - MDN:
rel="preload":as,type,mediaandcrossorigin. - MDN:
rel="prefetch": priority, caching and cache partitioning. - MDN: Speculation Rules API and
<script type="speculationrules">: prefetch, prerender, eagerness, constraints. - web.dev: Optimize resource loading with the Fetch Priority API:
fetchpriority, default priorities and support. - Chrome for Developers: Prerender pages: Chrome's limits and eagerness behaviour.