Back to Performance
Performance
easy
mid

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.

7 min read·~5 min to think through

Three resource-loading patterns that often get conflated. Each solves a different problem.

preload — "I need this NOW for this page"

html
<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) — wrong as means 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"

html
<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:

html
<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.

html
<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

ResourceUse
LCP hero imagepreload + fetchpriority=high (or eager)
Hero font (referenced in CSS)preload as=font
Critical above-fold scriptbundled into main; or modulepreload
Next route's JS / dataprefetch
Hover-likely chunkdynamic import on hover
Below-fold imagesloading=lazy
Sidebar iframe (YouTube embed, ad)loading=lazy
Third-party CDN hostpreconnect (or dns-prefetch)
Image carousel: current + next 2 slidespreload 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 crossorigin on 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

html
<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.

Senior engineer discussion

Seniors should be precise about *which* problem each hint solves: preload for current-page late-discovery, prefetch for next-navigation, lazy for offscreen defer. They don't conflate them. They also know preload is a sharp tool — overusing it causes contention with the very resources you're trying to speed up — and they validate hints with Network panel + LCP measurements rather than guessing.

Related questions