Explain lazy loading vs preloading vs prefetching
Lazy: load when needed. Preload: load now, high priority, current page. Prefetch: load idle-time, low priority, future navigation.
These three (plus their cousins preconnect, dns-prefetch, modulepreload) all express timing hints to the browser: when should this resource be fetched, how aggressively, and for which navigation? Using them right can shave seconds off LCP and make route transitions feel instant; using them wrong wastes bandwidth, contends with the critical path, and can actually slow LCP.
Lazy loading — defer until needed.
The default position. Don't fetch a resource until the user is about to need it. Implementations:
<img loading="lazy">— native browser API. The image is fetched only when it nears the viewport (the browser uses a heuristic threshold). Works for<iframe loading="lazy">too.React.lazy(() => import('./X'))ornext/dynamic— the JS chunk and its dependencies are fetched when the component actually mounts (or on Suspense boundary resolution).- Intersection-observer-driven manual lazy loading for custom widgets.
Use for: below-the-fold images, secondary route widgets, modal/dialog content, third-party embeds, large rich-media players.
Critical rule: don't lazy-load your LCP image. Lazy-loading the hero is counter-productive because the browser delays its fetch, and LCP rises. Mark the hero with loading="eager" (the default) and ideally fetchpriority="high".
**Preload — fetch now, high priority, for the current navigation.**
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin> tells the browser: "I will need this resource very soon on this page, start fetching it as soon as you parse this tag." Important: as is required — it determines priority class, CORS rules, and whether the response can be reused.
Use for: resources that are critical but discovered late by the parser. Examples:
- Fonts declared inside CSS (
@font-face) — the browser doesn't know to fetch them until it has applied the CSS, which can be hundreds of ms in. Preload them in the HTML head. - Hero images set via CSS
background-image— same problem. - Above-the-fold JSON that the page can't render without (use
as="fetch").
fetchpriority="high" on an <img> is a related signal for the LCP image without needing a separate preload link.
Don't use preload for things the parser would discover anyway (a normal <script src> or <img src> in the early HTML). Preload doesn't make them faster; it just adds noise and risks double-fetching if the as doesn't match exactly.
**Prefetch — fetch during idle time, low priority, for a future navigation.**
<link rel="prefetch" href="/dashboard"> tells the browser "the user will probably navigate here next; if you have spare bandwidth/CPU, go fetch it." Priority is lowest; the browser may skip the prefetch on slow networks or under Data Saver. The result is cached at the HTTP layer, so when the user clicks, the navigation feels instant.
Frameworks automate this:
- Next.js
<Link>prefetches automatically for links visible in the viewport (you can disable withprefetch={false}). This is why App Router navigations feel snappy. - Remix / TanStack Router have similar intent-based prefetching.
Use prefetch for likely-next pages, not for the current page. It's a speculation, not a guarantee.
Adjacent hints:
dns-prefetch—<link rel="dns-prefetch" href="//cdn.example.com">. Resolve the DNS name early. Very cheap; useful for third-party origins you'll hit eventually.preconnect—<link rel="preconnect" href="https://cdn.example.com" crossorigin>. DNS + TCP handshake + TLS negotiation. More expensive thandns-prefetchbut pays huge dividends for known third-party origins (analytics, image CDN, font CDN). Limit to 2–3; each preconnect ties up a connection slot.modulepreload—<link rel="modulepreload" href="/chunks/lib.js">. Like preload, but for ES modules; the browser fetches and parses the module and its dependency graph. Used by bundlers for critical chunks.- Speculation Rules API (newer) —
<script type="speculationrules">lets you specify URLs to prerender (full render, not just fetch). Use sparingly; prerendering executes JS.
Decision flow:
- Need it now, but browser hasn't discovered it yet → preload.
- Need it eventually on this page (scroll, click) → lazy.
- Need it on a likely-next page → prefetch.
- About to hit a known third-party origin → preconnect.
- Just want DNS resolved early → dns-prefetch.
- Need an ES module + its graph fetched early → modulepreload.
Pitfalls:
- Preloading the wrong
ascauses double fetches and warnings in DevTools. - Over-preloading clogs the critical path; the browser's bandwidth budget is finite.
- Prefetching too aggressively wastes mobile data — respect
Save-Dataheader andnavigator.connection.effectiveType. - Preload fonts must include
crossorigin(fonts are always CORS-fetched). - Verify in DevTools → Network → "Priority" column; you should see the resource at the priority you intended.
Code
Follow-up questions
- •When does prefetch hurt — over-eager fetching on slow networks?
- •How does Next.js decide what to prefetch?
- •What's modulepreload's role with ESM?
Common mistakes
- •Preloading everything → contention, cache eviction.
- •Lazy-loading critical above-the-fold content.
- •Forgetting `crossorigin` on preloaded fonts → double-fetch.
Performance considerations
- •Preload abused becomes a self-DoS — browser delays other critical resources.
Edge cases
- •Prefetch on metered connections is ignored by some browsers.
- •Service worker can intercept prefetches; coordinate caching strategies.
Real-world examples
- •Stripe Checkout preconnects to Stripe APIs from the merchant page so the checkout iframe handshake is already done.