How do preload, prefetch, and lazy loading differ in browser behavior?
Three different intents. preload: 'I need this for the CURRENT page, fetch it now at high priority' (LCP image, fonts). prefetch: 'I MIGHT need this for the NEXT navigation, fetch when idle' (next route's JS). Lazy loading: 'Don't load this until it scrolls into view or is otherwise needed' (offscreen images, dynamic imports). Don't mix them up — they have different priorities and timing.
Three resource-loading patterns that often get conflated. Each solves a different problem.
preload — "I need this NOW for this page"
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image">- Discovered by the browser as soon as HTML parses (often before the resource would naturally be discovered).
- High priority.
- Used for resources critical to the current page: LCP image, hero font, critical script not in main bundle.
- Must include
as(font/image/script/style) — wrongasmeans the preload doesn't match the eventual real request and you've just double-fetched. - Modulepreload variant:
<link rel="modulepreload" href="/main.mjs">for ES module entry points.
When to use: a resource the browser would otherwise discover late (e.g., a font URL in a CSS file — browser sees the font request only after CSS parses).
prefetch — "I MIGHT need this LATER"
<link rel="prefetch" href="/admin.js">- Low priority — fetched when the network is idle.
- Stored in HTTP cache; ready instantly if/when actually requested.
- Used for resources likely needed on the next navigation: code chunks for routes the user might visit, images for the next slide, etc.
Programmatic version: import('./next-route') on hover triggers the chunk fetch.
dns-prefetch and preconnect are different — they don't fetch a resource, they warm up DNS/TCP/TLS to a host:
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>Lazy loading — "Don't load until needed"
The opposite intent: defer loading until the resource is actually about to be visible.
<img src="below-fold.jpg" loading="lazy" alt="">
<iframe src="…" loading="lazy"></iframe>- Native lazy-loading on
<img>and<iframe>(no JS required). - IntersectionObserver for custom lazy components.
- Dynamic
import()for JS code (see code splitting). - Defer offscreen content: images below the fold, ads in sidebars, comments under articles.
Don't lazy-load the LCP image — that's a known anti-pattern. Use loading="eager" + fetchpriority="high" for the LCP image.
Decision table
| Resource | Use |
|---|---|
| LCP hero image | preload + fetchpriority=high (or eager) |
| Hero font (referenced in CSS) | preload as=font |
| Critical above-fold script | bundled into main; or modulepreload |
| Next route's JS / data | prefetch |
| Hover-likely chunk | dynamic import on hover |
| Below-fold images | loading=lazy |
| Sidebar iframe (YouTube embed, ad) | loading=lazy |
| Third-party CDN host | preconnect (or dns-prefetch) |
| Image carousel: current + next 2 slides | preload current; prefetch next |
Common mistakes
- Preloading too much — preload competes for bandwidth with the actual page. Use sparingly (1–3 resources).
- Preload without
as— silently no-op or double-fetch. - Missing
crossoriginon font preload — font request reissues without preload benefit. - Lazy-loading the LCP image — known LCP killer.
- Preloading a font that the page doesn't end up using — wasted bandwidth.
- Prefetching too aggressively — bandwidth+battery on mobile.
Measuring
- Chrome DevTools → Network → Priority column: preload should be High, prefetch Low.
- Lighthouse "Preload key requests" and "Avoid lazy-loading LCP image."
- Web Vitals (LCP especially) — the real measurement.
Modern bonus: fetchpriority
<img src="hero.avif" fetchpriority="high" alt="">
<script src="below.js" fetchpriority="low" async></script>fetchpriority (Chrome/Edge) lets you override the browser's default priority guess. Pair with preload for the LCP image to win on every browser.
Follow-up questions
- •Why is lazy-loading the LCP image bad?
- •When do you use preconnect vs dns-prefetch?
- •How does fetchpriority differ from preload?
- •What's the cost of over-prefetching on mobile?
Common mistakes
- •Lazy-loading the LCP image — drops LCP score significantly.
- •Preload without the correct as attribute — fetched but not used, double request.
- •Forgetting crossorigin on font preload — refetch.
- •Prefetching dozens of routes — bandwidth/battery hit on mobile.
- •Using preload for next-page resources — it's for current page; use prefetch instead.
- •Confusing dns-prefetch (DNS only) with preconnect (DNS+TCP+TLS).
Performance considerations
- •Preload moves LCP earlier by hundreds of ms on font-heavy or hero-image pages. Prefetch makes next-navigation feel instant (chunks already cached). Lazy loading drops initial bytes by deferring offscreen content — biggest wins on long article pages with many images. All three combined plus aggressive code splitting can take LCP from 4s to 1.5s on a typical content site.
Edge cases
- •Save-Data header: skip prefetch when the user has data-saving mode on.
- •Speculation Rules API (Chrome) is a newer, more powerful prerender/prefetch mechanism.
- •Quicklink and similar libraries prefetch visible links automatically.
- •Service workers can preemptively cache critical assets — independent of preload/prefetch hints.
- •HTTP/2 push was the old answer for this — deprecated; preload + 103 Early Hints is the modern replacement.
Real-world examples
- •Next.js <Link> prefetches in-viewport route chunks; <Image> sets fetchpriority on LCP image when priority prop is set.
- •YouTube embeds use loading=lazy heavily.
- •Pinterest and many image-heavy sites use IntersectionObserver-based lazy loading with low-res placeholders.