Resource Hints: Preload, Prefetch, Preconnect
Giving the Browser a Head Start
The browser discovers resources in a specific order: parse HTML, find <link>, find <script>, find <img>, find CSS url(), find JS fetch(). Each discovery point is sequential — the browser can't download what it hasn't found yet.
But what if you could whisper to the browser, "Hey, you're going to need this — start early"? That's what resource hints do. Done correctly, they shave hundreds of milliseconds off page load. Done incorrectly, they waste bandwidth and actually slow things down.
Imagine you're hosting a dinner party. Normally, you'd read through the recipe step by step, going to the store each time you discover a missing ingredient. Resource hints are like reading the full recipe first and sending someone to the store for everything at once. Preconnect is calling the store to confirm they're open. Preload is sending someone to buy a specific ingredient you know you'll need immediately. Prefetch is stocking up on ingredients you might need for tomorrow's dinner.
preconnect — Warm Up the Connection
Let's start with the simplest win. preconnect tells the browser to establish a connection to a server before it's needed. A full connection involves DNS lookup, TCP handshake, and TLS negotiation — each adding round-trip time.
<!-- Warm up connections to third-party origins you know you'll need -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preconnect" href="https://api.example.com" crossorigin>
When to use:
- Third-party CDNs that serve critical resources (fonts, analytics)
- API servers that the page will call immediately
- Any cross-origin server where you know the first request will happen soon
When NOT to use:
- More than 4-6 origins — each connection consumes CPU and memory. Preconnecting to many origins steals resources from actual requests.
- Origins used only on user interaction (e.g., a video CDN used only on play click)
If the resource will be fetched with CORS (fonts, fetch API calls), you must include crossorigin on the preconnect link. Without it, the browser establishes a non-CORS connection, then has to establish a second CORS connection when the actual resource is fetched — wasting the preconnect entirely.
dns-prefetch — The Lighter Alternative
dns-prefetch only resolves DNS — it doesn't establish a TCP/TLS connection. It's much cheaper and has broader browser support.
<!-- Cheaper than preconnect — DNS only, no TCP/TLS -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<!-- Use both for critical origins (dns-prefetch as fallback) -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
preload — Fetch This Resource Immediately
This is the big one. preload tells the browser to download a specific resource immediately because it's needed for the current page. The browser fetches it at high priority without waiting to discover it naturally.
<!-- Preload the hero image — normally discovered only after CSS parses -->
<link rel="preload" href="/hero.avif" as="image">
<!-- Preload a font — normally discovered only when CSS @font-face is parsed -->
<link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin>
<!-- Preload critical CSS loaded via JavaScript -->
<link rel="preload" href="/critical.css" as="style">
<!-- Preload a script needed immediately -->
<link rel="preload" href="/app.js" as="script">
The as attribute is critical — it tells the browser the resource type so it can:
- Apply the correct priority
- Send the correct
Acceptheader - Apply Content Security Policy checks
- Reuse the resource correctly (avoid double-fetching)
A preloaded resource that isn't used within ~3 seconds triggers a console warning: "The resource was preloaded using link preload but not used within a few seconds from the window's load event." Worse, it wasted bandwidth that could have loaded something actually needed. Only preload resources that the current page will use immediately. If you're not sure the page needs it, use prefetch instead.
What to Preload (and What Not To)
Good preload candidates:
- Fonts referenced in CSS — discovered late (after CSS downloads and parses)
- Hero images referenced in CSS
background-image— discovered after CSS parses - Critical scripts loaded dynamically via
import() - Above-the-fold images that are critical for LCP
Bad preload candidates:
- Below-the-fold images (waste bandwidth for non-urgent resources)
- Resources for other pages (that's prefetch territory)
- Too many resources — each preload competes for bandwidth with actual page resources
prefetch — Prepare for the Next Navigation
prefetch downloads resources that will be needed on a future page at low priority, using idle network time.
<!-- Prefetch the next likely page -->
<link rel="prefetch" href="/dashboard">
<!-- Prefetch a resource needed on the next page -->
<link rel="prefetch" href="/dashboard-data.json" as="fetch">
<!-- Prefetch a JavaScript chunk for a route the user will likely visit -->
<link rel="prefetch" href="/settings-chunk.js" as="script">
Prefetch is low priority — it only downloads when the browser has idle network bandwidth. It never competes with current-page resources.
When to use:
- Next-page resources that are predictable (e.g., the "next" page in a wizard flow)
- Code-split chunks for routes the user will likely visit
- Data that will be needed on user interaction
modulepreload — Preload for ES Modules
modulepreload is like preload but specifically for JavaScript modules. It downloads the module AND its dependency tree, and puts them in the module map ready for instantiation.
<!-- Preloads the module AND discovers/fetches its static imports -->
<link rel="modulepreload" href="/app.mjs">
Regular preload with as="script" only downloads the file. modulepreload also parses module dependencies and fetches those too — eliminating the waterfall where each module import triggers another network request.
fetchpriority — Fine-Tune Resource Priority
This one is newer and incredibly useful. fetchpriority lets you adjust the priority of individual resource requests within their default priority bucket:
<!-- LCP image — tell browser this is the most important image -->
<img src="/hero.jpg" fetchpriority="high" alt="Hero image">
<!-- Below-fold images — lower priority so they don't compete with hero -->
<img src="/footer-logo.png" fetchpriority="low" alt="Footer logo">
<!-- Critical CSS — high priority fetch -->
<link rel="stylesheet" href="/critical.css" fetchpriority="high">
<!-- Non-critical script — reduce priority -->
<script src="/analytics.js" fetchpriority="low" async></script>
Values: high, low, auto (default). This doesn't change the resource type's base priority — it adjusts within that tier.
Browser resource priority internals
Chrome assigns every request one of 5 internal priority levels: Highest, High, Medium, Low, Lowest. By default:
- CSS in
<head>: Highest - Fonts: Highest (when in view)
- Synchronous scripts in
<head>: High - Images in viewport: Medium (Low if not yet discovered in viewport)
- Prefetch: Lowest
fetchpriority="high" on an image bumps it from Medium to High — making it compete with scripts for bandwidth. This is exactly what you want for the LCP image: it should load as urgently as scripts. fetchpriority="low" on an image drops it to Low — it loads after all Medium-priority resources finish. Use this for below-fold images.
Production Scenario: The Font Flash
Here's a classic. A SaaS app had a noticeable flash of fallback font on every page load. The custom font (Inter) was defined in CSS, which was defined in a stylesheet, which had to download and parse before the browser even knew the font existed.
Timeline without hints:
- HTML downloads (200ms)
- CSS file discovered and downloads (300ms)
- CSS parsed,
@font-facediscovered (50ms) - Font file downloads (400ms)
- Total: 950ms before font renders
Fix:
<head>
<!-- Preconnect to font CDN -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload the specific font file — starts downloading immediately -->
<link rel="preload" href="https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuI6fMZs.woff2"
as="font" type="font/woff2" crossorigin>
<!-- CSS still loads normally -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700">
</head>
Timeline with hints:
- HTML downloads (200ms)
- Font and CSS start downloading in parallel immediately (preload + preconnect)
- Font ready at 400ms, CSS ready at 300ms
- Total: 400ms — font available before first paint
Result: Font flash eliminated on fast connections, reduced by 500ms on slow connections.
| What developers do | What they should do |
|---|---|
| Preload everything that might be needed Each preload competes for bandwidth with actual page resources. Unused preloads waste bandwidth and trigger console warnings. | Preload only resources needed immediately for the current page |
| Use preconnect for 10+ origins Each connection costs CPU, memory, and socket resources. Too many preconnects hurt performance instead of helping. | Limit preconnect to 4-6 critical origins |
| Use preload for next-page resources Preload is high priority for the CURRENT page. Using it for future pages steals bandwidth from current-page resources. | Use prefetch for next-page resources (low priority, idle bandwidth) |
| Forget crossorigin on font preloads Fonts are always fetched with CORS. Without crossorigin, the browser makes a non-CORS connection first, then a CORS one — double the work. | Always add crossorigin to preload/preconnect for CORS resources (fonts, fetch API) |
- 1preconnect establishes DNS + TCP + TLS early. Use for 4-6 critical third-party origins maximum.
- 2preload downloads a specific resource at high priority for the current page. Requires the 'as' attribute.
- 3prefetch downloads resources at low priority for future navigations. Uses idle bandwidth only.
- 4Always include crossorigin on preconnect and preload for CORS resources (fonts, fetch API calls).
- 5fetchpriority='high' on the LCP image is one of the simplest and most impactful LCP optimizations.
- 6Unused preloads waste bandwidth and trigger warnings. Audit preloads regularly — remove any that aren't used immediately.