Preload, Preconnect, and Resource Hints: When to Use Each
Resource hints are single-line HTML declarations that instruct the browser to perform network work -- DNS lookups, TCP connections, TLS handshakes, or full resource downloads -- before it actually needs them. Used correctly, they shave hundreds of milliseconds off Largest Contentful Paint and Time to First Byte by eliminating sequential round trips that sit on the critical rendering path. Used carelessly, they consume bandwidth, crowd out more important requests, and can actually make performance worse.
This guide covers every hint in the resource hints family -- preconnect, dns-prefetch, preload, prefetch, modulepreload, the Speculation Rules API, and 103 Early Hints -- and explains the precise conditions under which each one helps. By the end you will know exactly which hint to place, for which resource, and why.
- Add
preconnectfor up to 2-3 critical third-party origins -- DNS + TLS upfront saves 100-300 ms per origin. - Use
preloadonly for the LCP image and 1-2 above-the-fold fonts; always set theasattribute. - Use
prefetchfor next-page resources, not current-page critical assets. - Enable 103 Early Hints on your CDN -- it lets the browser act on hints before the server finishes generating the response.
- Validate every hint with WebPageTest connection waterfall to confirm it arrives early enough to matter.
The resource hints family overview
The browser's resource loading pipeline has several distinct phases: DNS resolution, TCP connection, TLS handshake, HTTP request, and response. Resource hints let you trigger any of these phases earlier than the browser would discover the need on its own. Each hint targets a different phase and carries different priority and caching implications.
Here is a concise reference for all seven hints:
<!-- dns-prefetch: resolve DNS only (HTTP/HTTPS, same security level) -->
<link rel="dns-prefetch" href="https://third-party.example.com">
<!-- preconnect: DNS + TCP + TLS handshake -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- preload: fetch current-page critical resource at high priority -->
<link rel="preload" href="/images/hero.webp" as="image" fetchpriority="high">
<!-- prefetch: fetch next-page resource at idle-time low priority -->
<link rel="prefetch" href="/about/hero.webp" as="image">
<!-- modulepreload: preload and parse an ES module + its dependencies -->
<link rel="modulepreload" href="/scripts/app.js">
<!-- Speculation Rules API: prerender or prefetch next pages (JSON) -->
<script type="speculationrules">
{ "prerender": [{ "source": "list", "urls": ["/checkout/"] }] }
</script>
<!-- 103 Early Hints: sent as an HTTP header before the 200 response -->
<!-- Link: <https://fonts.gstatic.com>; rel=preconnect; crossorigin -->
The key mental model: preconnect and dns-prefetch warm up connections to origins; preload and modulepreload fetch specific resources for the current page; prefetch and the Speculation Rules API prepare for future navigations; 103 Early Hints delivers all of the above from the server before the full response body exists.
preconnect in detail
Every connection to a new origin costs at least one network round trip. On a typical broadband connection with 40 ms RTT that is 40 ms for DNS, 40 ms for the TCP SYN-ACK, and 80 ms for the TLS 1.3 handshake (one round trip for the client hello and server hello, plus one for the finished messages in TLS 1.2 -- TLS 1.3 reduces this to one round trip). Total: roughly 160 ms of dead time before the first byte of a font, analytics script, or API response can arrive.
On mobile with 200 ms RTT -- not unusual for 4G in poor signal conditions -- that same sequence balloons to 800 ms. This is dead time during which your LCP element is waiting. preconnect moves the entire three-way handshake to the moment the HTML is parsed, overlapping it with the rest of the head processing.
<head>
<!-- GOOD: Full preconnect for origins that serve render-critical resources -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
<!-- OK: dns-prefetch only for lower-priority third-party origins -->
<!-- dns-prefetch has lower browser support cost and is fine for -->
<!-- origins you don't need until after the page is interactive -->
<link rel="dns-prefetch" href="https://analytics.example.com">
<link rel="dns-prefetch" href="https://comments-widget.example.com">
</head>
When to choose preconnect over dns-prefetch: Use preconnect when you will fetch a resource from that origin within the first few seconds of page load -- fonts, LCP images from a CDN, critical API calls. Use dns-prefetch for origins that are not needed immediately, or as a fallback for browsers that do not support preconnect (add both tags for the same origin in that case).
crossorigin. Without it, the browser opens an anonymous connection, and then opens a second CORS connection when the actual request fires, negating the hint entirely. Add crossorigin to any preconnect targeting a font origin or a CDN that serves assets with CORS headers.
Limit to 2-3 preconnects. Open connections consume memory on both the browser and the server. Each idle connection must be kept alive, and browsers have per-origin connection pool limits. More than 3 preconnects typically produce diminishing returns while adding overhead. Audit your page with Lighthouse -- the "Preconnect to required origins" audit will surface only the origins that actually delay your critical path.
preload done right
preload is the most powerful and most abused resource hint. It instructs the browser to start downloading a specific resource at high priority the moment it reads the <link> tag, regardless of where that resource would normally be discovered in the parsing flow. This is critical for LCP because font files are not discoverable until the browser has parsed the CSS that references them -- often several seconds into the load sequence.
The as attribute is mandatory. Without it, the browser fetches the resource at lowest priority and does not apply the correct request headers, which causes a cache miss when the actual request arrives. Every preload must have an as value that matches the resource type.
<head>
<!-- LCP image: fetchpriority=high overrides browser heuristics -->
<link
rel="preload"
href="/images/hero-800.webp"
as="image"
fetchpriority="high"
>
<!-- Responsive LCP image: browser picks the right srcset entry -->
<link
rel="preload"
as="image"
imagesrcset="/images/hero-400.webp 400w,
/images/hero-800.webp 800w,
/images/hero-1600.webp 1600w"
imagesizes="(max-width: 600px) 100vw, 800px"
fetchpriority="high"
>
<!-- Critical font: crossorigin is required even for same-origin fonts -->
<link
rel="preload"
href="/fonts/inter-var-latin.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- Conditional preload: only load large desktop image on wide viewports -->
<link
rel="preload"
href="/images/hero-desktop.webp"
as="image"
media="(min-width: 1024px)"
>
</head>
fetchpriority on preload: Chrome 102+ supports the fetchpriority attribute with values high, low, and auto. For the LCP element, set fetchpriority="high" on the preload. This boosts the request above other high-priority resources like render-blocking CSS. The responsive images LCP fix guide covers srcset-based preloading in detail, including how to match the imagesrcset value to the <img srcset> attribute exactly.
For web fonts, the interaction with web font performance is important: preloading a font that uses font-display: swap narrows the window between page render and font swap, reducing the CLS exposure window. But preloading a font that uses font-display: block can actually worsen LCP by blocking text rendering while the font downloads.
prefetch vs. preload
The distinction is straightforward but frequently confused: preload is for the current page navigation; prefetch is for the next navigation. The browser treats them with very different priority.
A preload hint fires immediately after the HTML parser reads it and competes for bandwidth with other high-priority requests. A prefetch hint is placed in the browser's idle-time queue and only executes when the network is otherwise idle -- it will not start at all on a slow connection under load, which is exactly the behavior you want for non-critical future resources.
<head>
<!-- Prefetch: the user is likely to navigate to /checkout/ -->
<link rel="prefetch" href="/checkout/" as="document">
<link rel="prefetch" href="/checkout/bundle.js" as="script">
<link rel="prefetch" href="/images/checkout-hero.webp" as="image">
<!-- NOT this: do not prefetch analytics or tracking pixels -->
<!-- <link rel="prefetch" href="https://analytics.example.com/collect"> -->
</head>
Prefetched resources are stored in the HTTP cache with normal cache semantics. If the server responds with Cache-Control: no-store, the prefetch is wasted -- nothing is stored. If the server uses stale-while-revalidate, the prefetch result can be served instantly on the next navigation while a background revalidation runs. This makes prefetch particularly effective for pages with short-lived but cacheable HTML responses.
Never use prefetch for analytics endpoints. Prefetching a tracking URL sends a phantom pageview or event before the user actually navigates, corrupting your analytics data. This is a surprisingly common mistake when engineers copy prefetch patterns without reading the as values.
modulepreload is a variant of preload specifically for ES modules. It not only fetches the module file early but also parses and compiles it, and recursively prefetches its static imports. For applications with a deeply nested module graph, a single modulepreload at the top of the graph can eliminate multiple sequential round trips. See the render-blocking resource fix guide for module loading patterns in the critical path.
103 Early Hints and the Speculation Rules API
Both of these newer APIs push resource hints earlier in the pipeline than in-HTML <link> tags can achieve.
103 Early Hints
The HTTP 103 status code lets a server or CDN edge node send Link response headers before it has finished generating the full 200 response. During the server think time -- while the origin fetches data from a database, renders a template, or calls a microservice -- the browser receives the 103 response and immediately starts processing the preconnect and preload hints inside it.
This is especially powerful for TTFB optimization. A page with 200 ms server think time can use that 200 ms to preconnect to CDN origins, start downloading the LCP image, and warm up font connections -- all before the 200 response arrives. The TTFB fix on Vercel and the CDN optimization guide cover CDN-specific Early Hints configuration.
# Server sends this immediately, before the 200 response is ready:
HTTP/1.1 103 Early Hints
Link: <https://fonts.gstatic.com>; rel=preconnect; crossorigin
Link: <https://cdn.example.com>; rel=preconnect
Link: </images/hero.webp>; rel=preload; as=image; fetchpriority=high
# Then, after the server finishes processing:
HTTP/1.1 200 OK
Content-Type: text/html
...
# Cloudflare configuration (via Response Header Transform Rule):
# Set "Link" header to the above values on HTML responses
As of 2025, 103 Early Hints is supported in Chrome 103+, Firefox 120+, and Safari 17.2+, and can be served from Cloudflare (enabled per zone), Fastly, and nginx 1.25.1+. Apache does not yet have native support.
Speculation Rules API
The Speculation Rules API is the successor to rel=prerender and goes further than prefetch by fully pre-rendering the next page in a hidden tab -- running its JavaScript, fetching its subresources, and executing its paint steps. When the user actually navigates, the transition is near-instant.
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/checkout/", "/products/top-pick/"],
"eagerness": "moderate"
}
],
"prefetch": [
{
"source": "document",
"where": { "href_matches": "/blog/*" },
"eagerness": "conservative"
}
]
}
</script>
When to use speculation: Prerender is appropriate for high-confidence next steps with a clear funnel -- product detail to checkout, article list to article. Do not prerender pages that have side effects (form submissions, add-to-cart endpoints) or that contain user-specific content that would be stale. The eagerness property controls when speculation fires: conservative only on pointer hover, moderate on link hover with a short delay, eager immediately on discovery.
Common mistakes
-
Preload everything syndrome. Adding
rel=preloadto every script, stylesheet, and image is the single most common resource hints mistake. Each preload injects a high-priority request that competes with the HTML document, render-blocking CSS, and each other. The result is a bandwidth contention storm that can increase LCP by 300-500 ms compared to no hints at all. Preload only the LCP element and 1-2 fonts. -
Missing crossorigin on font preload. Fonts are always fetched with CORS mode, even for same-origin fonts, because the CSS spec requires it. A preload for a font without the
crossoriginattribute will result in two requests: one non-CORS preload that is wasted, and one CORS request when the font-face rule fires. Chrome DevTools will warn about this in the Network panel. Addcrossoriginto every font preload. -
Preloading below-the-fold images. Preloading an image in a carousel that is below the fold wastes the first 200-400 ms of network bandwidth on a low-priority asset that is not the LCP element. Use
loading="lazy"on below-the-fold images and reserve preloads strictly for the hero image or the resource that is the Largest Contentful Paint. See the responsive images fix guide for proper LCP image identification. -
Duplicate preload plus regular link tag. A common copy-paste error: a developer adds
<link rel=preload href=style.css as=style>and also keeps<link rel=stylesheet href=style.css>. Modern browsers deduplicate these into a single request, but the preload fires before the stylesheet link, which is correct. The problem arises when the preload uses a different URL than the stylesheet (different query string, protocol variant) -- then two separate requests are made. Make sure the preloadhrefmatches the actual asset URL exactly. -
Preloading based on User-Agent. Server-side resource hint injection that varies by user agent (e.g., "send a preload for the mobile image to mobile UAs") can cause cache poisoning or wasted loads when CDN caches serve the wrong variant. Use
mediaqueries on client-side preload hints instead, or ensure your CDN has properVary: User-Agentheaders set before doing server-side UA-based hinting.
Step-by-step fix
Step 1: Identify critical third-party origins with Lighthouse
Open Chrome DevTools, run a Lighthouse performance audit, and look for the "Preconnect to required origins" opportunity. Lighthouse identifies third-party origins that participate in your critical rendering path but do not have an early connection established. Also check the Network waterfall in DevTools: filter by "third-party" and look for any request that starts a new connection after the 500 ms mark.
// Run in DevTools console to find origins with high connection overhead
const entries = performance.getEntriesByType('resource');
const thirdPartyStats = {};
const ownOrigin = location.origin;
entries
.filter(e => !e.name.startsWith(ownOrigin))
.forEach(e => {
const url = new URL(e.name);
const origin = url.origin;
if (!thirdPartyStats[origin]) {
thirdPartyStats[origin] = { count: 0, dnsTime: 0, connectTime: 0 };
}
thirdPartyStats[origin].count++;
thirdPartyStats[origin].dnsTime += e.domainLookupEnd - e.domainLookupStart;
thirdPartyStats[origin].connectTime += e.connectEnd - e.connectStart;
});
console.table(thirdPartyStats);
Step 2: Add preconnect for up to 2-3 critical third-party origins
For each critical origin identified in step 1, add a <link rel="preconnect"> tag as early as possible in the <head> -- before any stylesheets or scripts. For origins that serve fonts or other CORS resources, include the crossorigin attribute. For lower-priority origins, add dns-prefetch as a fallback.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Critical origins: preconnect (max 2-3) -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.mysite.com">
<!-- Lower-priority origins: dns-prefetch only -->
<link rel="dns-prefetch" href="https://analytics.mysite.com">
<!-- Stylesheets after preconnects -->
<link rel="stylesheet" href="/styles/main.css">
</head>
Step 3: Preload the LCP image and 1-2 critical fonts only
Identify your LCP element using Lighthouse or the PerformanceObserver API with largest-contentful-paint entries. If it is an image, add a rel=preload with as=image and fetchpriority="high". For responsive images, use imagesrcset and imagesizes to match your <img srcset> declaration exactly. If the LCP element is text rendered in a custom font, preload that font file with as=font and crossorigin.
<head>
<!-- LCP image (fixed size) -->
<link
rel="preload"
href="/images/hero-1200.webp"
as="image"
fetchpriority="high"
>
<!-- LCP image (responsive) -->
<link
rel="preload"
as="image"
imagesrcset="
/images/hero-400.webp 400w,
/images/hero-800.webp 800w,
/images/hero-1200.webp 1200w"
imagesizes="(max-width: 640px) 100vw, 50vw"
fetchpriority="high"
>
<!-- LCP font (if the LCP element is text) -->
<link
rel="preload"
href="/fonts/cabinet-grotesk-800.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- Body font (1 file max) -->
<link
rel="preload"
href="/fonts/satoshi-variable.woff2"
as="font"
type="font/woff2"
crossorigin
>
</head>
Step 4: Configure Early Hints in your CDN or server
103 Early Hints delivers the same preconnect and preload hints from the server before the full response is ready, so the browser can act on them during server think time. On Cloudflare, enable Early Hints in the Speed settings for your zone. On nginx 1.25.1+, use the http2_early_hints directive. On Fastly, use the beresp.http.Link VCL variable. Include only the 2-3 most critical hints -- the same ones you would put in your HTML head.
# nginx 1.25.1+ with HTTP/2 enabled
server {
listen 443 ssl http2;
location / {
# Send 103 before proxying to upstream
http2_early_hints on;
add_header Link "</fonts/satoshi-variable.woff2>; rel=preload; as=font; crossorigin";
add_header Link "</images/hero-1200.webp>; rel=preload; as=image; fetchpriority=high";
add_header Link "<https://cdn.example.com>; rel=preconnect";
proxy_pass http://upstream;
}
}
// Node.js 18.11+ supports res.writeEarlyHints()
app.get('/', (req, res) => {
res.writeEarlyHints({
link: [
'</fonts/satoshi-variable.woff2>; rel=preload; as=font; crossorigin',
'</images/hero-1200.webp>; rel=preload; as=image; fetchpriority=high',
'<https://cdn.example.com>; rel=preconnect',
],
});
// Normal async work (DB query, rendering, etc.)
const html = await renderPage();
res.send(html);
});
Step 5: Validate with WebPageTest connection waterfall
Run a WebPageTest test from a location geographically distant from your server to amplify any remaining connection overhead. In the result view, select "Connection View" to see every DNS lookup, TCP handshake, and TLS negotiation. Verify that: (1) preconnected origins show zero DNS and connect time when first used, (2) the LCP resource starts downloading before the 1-second mark, and (3) there are no wasted preloads -- unused preloads will appear in the waterfall with no download completion within the page load window.
// Run in DevTools console after page load
// Finds resources fetched via preload (initiatorType = 'link')
const preloads = performance
.getEntriesByType('resource')
.filter(e => e.initiatorType === 'link');
preloads.forEach(e => {
const url = new URL(e.name);
console.log({
file: url.pathname,
startTime: e.startTime.toFixed(0) + ' ms',
duration: e.duration.toFixed(0) + ' ms',
// transferSize === 0 means served from cache (good on revisit)
transferSize: e.transferSize + ' bytes',
});
});
// Check LCP element
new PerformanceObserver(list => {
const lcp = list.getEntries().at(-1);
console.log('LCP element:', lcp.element?.tagName, 'at', lcp.startTime.toFixed(0) + ' ms');
}).observe({ type: 'largest-contentful-paint', buffered: true });
Quick checklist
-
No more than 2-3
preconnecttags; lower-priority origins usedns-prefetch -
All
preconnecthints for font or CORS origins include thecrossoriginattribute -
LCP image is preloaded with
as=imageandfetchpriority="high" -
Responsive LCP image preload uses
imagesrcsetandimagesizesmatching the<img>attributes exactly -
At most 1-2 font preloads; all font preloads include
crossoriginandtype="font/woff2" - 103 Early Hints configured on CDN or server for highest-impact hints
- WebPageTest connection waterfall confirms no orphaned preloads and early connection resolution
Frequently asked questions
preload fetches a resource needed for the current page at high priority as soon as the browser reads the link tag. prefetch fetches a resource at idle-time low priority for use in a future navigation. Use preload for the LCP image or critical fonts on the current page. Use prefetch for resources required on the next page the user is likely to visit. Mixing them up -- using prefetch for current-page critical assets or preload for next-page assets -- either delays the current load (prefetch has no effect on current page priority) or wastes bandwidth at the worst possible time (preload fires immediately and competes with the main document).
Yes -- if you use Google Fonts, add preconnect to both https://fonts.googleapis.com and https://fonts.gstatic.com. The first serves the CSS file; the second serves the actual font files. Add crossorigin on the gstatic origin because font files are fetched with CORS mode. However, the most performant approach is to self-host the fonts entirely. Self-hosting eliminates both connections, enables direct font file preloads, and gives you control over cache headers and font subsetting. See the web fonts performance guide for a complete self-hosting walkthrough.
More than 3-4 preloads on a single page is almost always counterproductive. Every preload injects a high-priority request that competes for bandwidth with the HTML document and render-blocking CSS. Chrome DevTools also warns about unused preloads -- resources preloaded but not consumed within 3 seconds -- which indicates the hint fired too early or pointed at the wrong resource. Limit preloads to the LCP element (one image or one font file) plus at most 1-2 additional above-the-fold fonts. Never preload below-the-fold content.
Yes. All resource hints work across HTTP/1.1, HTTP/2, and HTTP/3. On HTTP/2 and HTTP/3, preconnect is still valuable for third-party origins because even a multiplexed connection requires a TLS handshake (TLS over TCP for HTTP/2, TLS over QUIC for HTTP/3). The handshake savings are the same regardless of protocol version. HTTP/2 Server Push was once an alternative to preload for same-origin assets, but it has been deprecated by most CDNs -- Cloudflare, Fastly, and Chrome removed or disabled it -- in favor of 103 Early Hints, which provides the same benefit without the cache-poisoning risks.
HTTP 103 Early Hints is a status code that allows a server or CDN to send Link headers containing preconnect and preload directives before the full 200 response is generated. The browser processes these hints during server think time, so by the time the HTML arrives, critical connections are already established and LCP resources are already downloading. Browser support as of 2025: Chrome 103+, Firefox 120+, Safari 17.2+. Server/CDN support: Cloudflare (enable in Speed settings), Fastly (via VCL), nginx 1.25.1+ (http2_early_hints directive), Node.js 18.11+ (res.writeEarlyHints()). Apache does not yet have native 103 support.
Sara Kim
Performance Engineer
Sara specializes in browser loading pipelines, network optimization, and Core Web Vitals measurement. She has audited performance for e-commerce, media, and SaaS products across all major frontend frameworks.
Related fixes
Responsive Images for LCP
Correctly size, format, and preload responsive images to hit a Good LCP score across all viewport sizes.
FixWeb Fonts Performance
Self-host fonts, subset to Latin, and configure fallback metrics to eliminate font-loading overhead from LCP and CLS.
FixCDN Optimization for LCP
Configure CDN caching, edge locations, and 103 Early Hints to serve your LCP asset from the closest possible origin.