Resource Hints in 2026: preload vs prefetch vs preconnect vs dns-prefetch
A practical 2026 guide to the four browser resource hints (preload, prefetch, preconnect, dns-prefetch): when each one helps, when it hurts LCP, and the exact decision flow to pick the right one.
Resource hints (preload, prefetch, preconnect, and dns-prefetch) are <link> directives that tell the browser to start network work earlier than it normally would. Preload fetches a resource needed for the current page, prefetch fetches one needed for a future page, preconnect opens a TCP+TLS connection to an origin you're about to use, and dns-prefetch only resolves the DNS for that origin. Using the wrong one is the most common cause of "I added a preload and my LCP got worse" complaints in 2026, and this guide shows exactly which hint to reach for in each scenario.
preload is for resources the current page will use (LCP image, critical font, deferred script). It does not execute the resource, only fetches it.
prefetch is for resources the next navigation will use; it runs at Lowest priority and is stored in the HTTP cache, not the memory cache.
preconnect opens DNS + TCP + TLS to an origin in advance. Use it for the 3–4 most critical third-party origins, no more.
dns-prefetch only resolves the hostname; use it as a cheap fallback when you have many third-party origins and can't afford to preconnect them all.
Combining preload with fetchpriority="high" is the modern replacement for old "preload everything critical" advice. Over-preloading still demotes your real LCP candidate.
HTTP 103 Early Hints can deliver the same preconnect/preload directives during server think-time, multiplying their effect.
What are resource hints and why do they matter?
Resource hints are declarative instructions a developer adds to HTML (or sends as an HTTP Link header) that tell the browser to start network work earlier than its default speculative parser would. They were standardized in the W3C Resource Hints specification and have since been folded into the WHATWG HTML Living Standard alongside newer additions like fetchpriority and Early Hints.
The reason they matter for Core Web Vitals is timing. A modern page does roughly the same amount of network work whether you add hints or not. What hints change is when that work starts. Moving DNS resolution, TCP handshake, TLS handshake, and the fetch itself ahead of the moment the parser discovers the resource can cut hundreds of milliseconds off Largest Contentful Paint and First Contentful Paint, especially on mid-tier mobile devices on 4G networks where the round-trip overhead dominates download time.
Honestly, in my experience auditing sites in 2026, the single biggest perf win on poorly-tuned pages is usually removing stale or wrong resource hints rather than adding new ones. The four hints look similar but do very different things, and confusion between them is the rule, not the exception.
Resource hints at a glance: a comparison table
Before diving into each hint individually, here's the cheat sheet I keep open when reviewing a critical path.
Property
preload
prefetch
preconnect
dns-prefetch
Intended for
Current page
Next page
Any near-future origin
Any near-future origin
What it does
Fetches the resource
Fetches at lowest priority
DNS + TCP + TLS
DNS only
Cache location
In-memory preload cache
HTTP cache (disk)
Connection pool
OS / browser DNS cache
Default priority
Inherits from as + fetchpriority
Lowest
n/a (no transfer)
n/a (no transfer)
Requires as=
Yes (or the request is wasted)
Optional
No
No
Requires crossorigin for fonts/CORS
Yes
Optional
Yes, when needed
No
Risk when overused
Demotes real LCP candidate
Wastes user bandwidth
Exhausts socket pool
Negligible (cheap)
Best paired with
fetchpriority
Speculation Rules API
dns-prefetch fallback
n/a
preload: fetch what the current page will use
The <link rel="preload"> hint tells the browser: "I'm going to need this exact resource for the current navigation, fetch it now at the priority I'm declaring." It does not evaluate, execute, parse, or apply the resource. That still happens when the markup or CSS that actually consumes it reaches the parser. The fetch lands in a short-lived in-memory preload cache that the consuming element picks up later.
The two attributes that make or break a preload are as and crossorigin. The as attribute sets the request destination, which controls Accept headers, CORS mode, and default priority. Forgetting as means the browser issues the fetch but won't reuse the result, so the resource gets downloaded twice. Classic footgun.
<!-- Preload the LCP hero image at the highest priority -->
<link rel="preload"
as="image"
href="/hero-1600.avif"
imagesrcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
imagesizes="100vw"
fetchpriority="high">
<!-- Preload a self-hosted font (always needs crossorigin, even same-origin) -->
<link rel="preload"
as="font"
type="font/woff2"
href="/fonts/Inter-var.woff2"
crossorigin>
<!-- Preload a deferred module script -->
<link rel="modulepreload" href="/js/cart.js">
Note that fonts always require crossorigin on the preload, even when same-origin. Font requests are issued in CORS mode and a non-CORS preload will not match. Modules use the sibling hint modulepreload, which additionally fetches and parses the module's dependency graph. Pair preload with our complete guide to fetchpriority and Priority Hints for fine-grained control over what wins the bandwidth race.
prefetch: fetch what the next page will use
Where preload serves the current navigation, prefetch serves the next one. The browser fetches the resource at Lowest priority once the network is idle and stores it in the HTTP cache so a subsequent navigation can pull it from disk. Because the priority is Lowest, prefetch does not compete with critical resources on the current page. The cost is mostly user bandwidth.
In 2026 the Speculation Rules API has largely superseded plain <link rel="prefetch"> for navigation prefetching because it can prefetch whole documents (not just sub-resources), handle dynamic URL patterns, and gate eagerness on user signals like hover or pointerdown. Use the <link> form only for static, certain-next-resource cases (like a multi-step checkout's next bundle), or as a fallback for older browsers.
One subtlety: a prefetched resource for the next navigation lives in the HTTP cache, not the memory preload cache. That means it's subject to Cache-Control: if the server sent no-store, prefetch is a no-op. Check headers before assuming a prefetch is working. (I've lost an embarrassing amount of debugging time to that exact gotcha.)
preconnect: warm up a connection before you need it
A new HTTPS connection costs at minimum 1 DNS lookup, 1 TCP round-trip, and 1 TLS round-trip. On a 4G mobile network in a coverage-poor area, that's easily 300–600ms before a single byte of payload moves. <link rel="preconnect"> lets you eat that cost during otherwise-idle time so the actual fetch later finds a warm connection ready to reuse.
<!-- Image CDN we'll fetch the LCP from -->
<link rel="preconnect" href="https://images.example-cdn.com">
<!-- Google Fonts: stylesheet domain AND font-file domain, latter is cross-origin -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
The right rule of thumb is "no more than three or four preconnects per page." Each open connection consumes a socket and competes for the browser's connection pool, and a page that preconnects to 12 third-party origins will exhaust the pool, queue real requests, and end up slower than the same page with zero hints. If you have more than four origins worth warming, preconnect the top three by impact and use dns-prefetch on the rest.
Use crossorigin on the preconnect when the origin will be fetched in CORS mode (fonts, fetch() with credentials: 'include' to a different origin, etc.). Mismatched CORS mode is the single biggest cause of "preconnect doesn't seem to work" reports. The browser opens a non-CORS connection that the real CORS request can't reuse, so it opens a second one anyway. MDN's preconnect reference has the full CORS matching matrix.
dns-prefetch: the cheap fallback for many origins
If preconnect is the premium tier, dns-prefetch is the economy option. It performs only the DNS resolution (no TCP, no TLS), so it costs almost nothing in client resources and works at huge scale. Every third-party origin you may eventually hit can have a dns-prefetch without harming the page.
<!-- Preconnect the top 2-3 critical origins -->
<link rel="preconnect" href="https://images.example-cdn.com">
<link rel="preconnect" href="https://api.example.com">
<!-- dns-prefetch the long tail of less-critical origins -->
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://cdn.adserver.example">
<link rel="dns-prefetch" href="https://widget.support.example">
The pattern I ship on most production sites is "preconnect the same origins again as dns-prefetch." Modern browsers ignore the redundant directive cleanly, but in the rare case a preconnect fails (out of sockets, network throttling), the DNS resolution still happens and the eventual fetch saves a few hundred ms anyway. It costs nothing to belt-and-brace these.
What is the difference between preload and prefetch?
The short answer: preload is for now, prefetch is for later. Preload tells the browser the resource will be used on this navigation and fetches it at the priority you declare (often High). Prefetch tells the browser the resource will be used on a future navigation and fetches it at the Lowest priority, after the current page has finished its critical work.
The mechanical differences cascade from that intent:
Priority: preload inherits high/medium from as+fetchpriority; prefetch is always Lowest.
Cache: preload uses a short-lived in-memory cache; prefetch writes to the persistent HTTP cache.
Cancellation: a preload not consumed within ~3 seconds logs a console warning and is discarded. A prefetch sits in the HTTP cache until it expires or the cache evicts it.
CORS: preload's CORS mode must match the eventual fetch exactly. Prefetch is more forgiving because the receiving page will issue its own request with its own CORS mode.
A common antipattern is using preload for the next-page route bundle to "make navigation instant." That preload competes with current-page resources at High priority, hurting current-page LCP without actually speeding up the next navigation any more than prefetch (which doesn't compete) would. I hit this exact bug shipping a checkout redesign last year, and it cost a full point of LCP regression before we caught it in CrUX.
How preload works with fetchpriority in 2026
Before fetchpriority shipped, "preload" was the only blunt instrument we had to tell the browser "this matters." That created the over-preloading problem: every team marked their hero image, their LCP font, their critical CSS, and their above-the-fold script as preload, and the browser had no way to know which of those five preloads was the actual LCP candidate. Net effect: the real LCP often got demoted by the noise.
Since Chrome 102 and now stable across all modern browsers, fetchpriority="high" on the actual <img> for your LCP candidate is almost always the right move (no preload required). The browser's speculative parser already discovers the image before the layout pass; fetchpriority just bumps it past the default Low for images-below-the-fold.
<!-- Modern pattern: declare priority on the image itself -->
<img src="/hero.avif"
fetchpriority="high"
width="1600" height="900"
alt="...">
<!-- Use preload only when the resource is NOT in the static HTML -->
<link rel="preload"
as="image"
href="/dynamic-hero.avif"
fetchpriority="high">
The rule of thumb I use: prefer fetchpriority directly on the element when the element is in the initial HTML. Reach for <link rel="preload"> only when the resource is loaded by CSS (background images, fonts), by JavaScript (dynamic imports), or arrives via a route the parser can't see. For an LCP deep dive that covers TTFB, resource load delay, and render delay, see our guide to optimizing every LCP sub-part.
Delivering hints via HTTP 103 Early Hints
Resource hints work even better when they arrive before the HTML does. HTTP 103 Early Hints is a response status that lets your origin send Link headers with rel=preconnect or rel=preload during server think-time, which is the time spent in your application code generating the actual 200 response. The browser starts the connection or fetch immediately on receiving the 103, then the 200 final response arrives with the real HTML. The behavior is specified in RFC 8297.
HTTP/1.1 103 Early Hints
Link: <https://images.example-cdn.com>; rel=preconnect
Link: </css/critical.css>; rel=preload; as=style
Link: </hero.avif>; rel=preload; as=image; fetchpriority=high
HTTP/1.1 200 OK
Content-Type: text/html
...the actual page...
If your origin takes 400ms to generate HTML, Early Hints can absorb most of that time for connection and asset warmup. The hints in the eventual 200 response are still respected and merged with the early ones, so you don't need to dedupe. For implementation details across Node, Rails, Go, and major CDNs (Cloudflare, Fastly, Cloudfront), see our walkthrough on HTTP 103 Early Hints to cut TTFB and LCP.
Common resource-hint mistakes (and how to fix them)
Five mistakes account for the vast majority of broken hint deployments I see in audits:
Preload without as: The browser fetches the resource but won't match it to the consuming request, so the real fetch goes out again. Always set as= matching the eventual destination (script, style, image, font, fetch).
Font preload without crossorigin: Fonts are CORS even when same-origin. Missing crossorigin means the font is downloaded twice. The fix is a single attribute.
Too many preconnects: More than four preconnects per page exhausts the socket pool. Audit with DevTools "Network → Connections" view and trim the list.
Preloading the next page's bundle as if it were current: Use rel="prefetch" or Speculation Rules instead. Preloading next-page resources steals priority from current-page work and never delivers the speedup you wanted.
Preload + loading="lazy" on the same image: They cancel each other out. Either preload + eager render, or no preload + lazy.
A decision flow: which hint do I actually need?
So, when I'm deciding which hint (if any) to add for a specific resource, the question chain is:
Is the resource used on the current page? If yes → preload (or fetchpriority directly on the element if it's in the static HTML). If no → prefetch or Speculation Rules.
Is the resource on another origin? If yes → add a preconnect to that origin before the preload.
Do I have more than ~4 third-party origins? If yes → preconnect the top three, dns-prefetch the rest.
Does my origin take >200ms to generate HTML? If yes → move the hints into HTTP 103 Early Hints headers.
Is the resource a font? Always add crossorigin.
Will the resource be rendered lazily? If yes → do not preload it.
That decision flow has caught more bad hints in my reviews than any documentation I've handed teams. The hints themselves are simple; the trap is in remembering which one means what when you're three weeks into a redesign and someone asks "should we preload the homepage CTA bundle?"
Frequently Asked Questions
When should I use preconnect vs dns-prefetch?
Use preconnect for the 3–4 third-party origins most critical to your page (CDN, image host, primary API). Use dns-prefetch for the long tail of less-critical origins. Preconnect costs socket+TLS resources; dns-prefetch is nearly free, so you can use it liberally. A common pattern is to do both for critical origins as a fallback.
Does rel=preload still matter in 2026 with fetchpriority?
Yes, but its role has narrowed. For resources in the static HTML (<img>, <script>), fetchpriority="high" on the element itself is preferred, because the preload scanner already finds them. Reach for preload when the resource is loaded by CSS (background images, fonts), JavaScript (dynamic imports), or otherwise hidden from the parser.
Why does my preloaded font get downloaded twice?
The <link rel="preload"> for a font is missing the crossorigin attribute. Font requests are always issued in CORS mode, even for same-origin fonts, and a non-CORS preload doesn't match a CORS request, so the browser issues a second fetch. Add crossorigin to every font preload.
Can I preload a resource that loads lazily?
No, they cancel each other out. loading="lazy" defers the fetch until the image scrolls near the viewport, which discards any preload you set up. Pick one: preload+eager for above-the-fold critical images, or lazy (no preload) for below-the-fold images.
How many preconnects is too many?
More than four preconnects per page typically hurts more than it helps. Each open connection consumes a socket from the browser's pool and competes for limited TLS state. Audit your hints in Chrome DevTools (Network panel → Connection View) and keep preconnects to the top 3–4 critical origins; use dns-prefetch for the rest.
Client Hints (Sec-CH-DPR, Sec-CH-Width, Critical-CH) let your CDN pick byte-perfect image sizes in 2026. Practical setup, worker code, and real LCP wins from a Chrome-heavy audit.
Learn how RFC 9211's Cache-Status header exposes hits, misses, forwards, and coalesced requests across your CDN chain. Includes vendor mapping and TTFB attribution patterns.
Tag any image, price, or button with elementtiming and read its real paint timestamp from PerformanceObserver. Practical RUM setup, cross-origin gotchas, and the new Container Timing trial.