frontend / performance / 08_resource_hints.md

Resource Hints — preload, prefetch, preconnect, dns-prefetch, modulepreload

6 min read source

Resource Hints — preload, prefetch, preconnect, dns-prefetch, modulepreload

TL;DR

Resource hints tell the browser to start work earlier than its default discovery would. Five common ones, each at a different layer:

Hint Tells the browser to When
dns-prefetch resolve the DNS for an origin early, very cheap
preconnect open the TCP+TLS to an origin (warm the connection) when you know a request to that origin is imminent
preload fetch a specific resource ASAP at high priority critical CSS, fonts, LCP image, key JS
prefetch fetch a resource for future navigation, low priority next-page resources during idle
modulepreload preload + parse an ESM module and its dependency graph critical JS modules

Used surgically, they shave hundreds of milliseconds off LCP. Used liberally, they fight your own bandwidth and hurt the things that actually matter. Measure.

Interview Q&A

Q: When use dns-prefetch?

A: When you’ll connect to a third-party origin you know about up-front but haven’t connected to yet.

<link rel="dns-prefetch" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />

DNS resolution is cheap (typically 20-100ms) but a real serial step. Doing it early in parallel with HTML parsing saves that time when the request fires.

Use for: known third-party origins (analytics, ad servers, fonts.googleapis.com when used, CDN domains). Skip for: the origin already serving the HTML (you’re already connected) or origins you’ll preconnect to (preconnect supersedes).

Q: preconnect vs dns-prefetch?

A: preconnect does DNS + TCP + TLS ahead of time — opens a warm connection.

<link rel="preconnect" href="https://cdn.example.com" />
<link rel="preconnect" href="https://api.example.com" crossorigin />
  • ~250-1000ms saved per connection on fresh visits (cold cache).
  • More expensive than dns-prefetch — opens a real connection. Limit to 3-4 critical origins.
  • crossorigin attribute needed if you’ll fetch with credentials (or for fonts).

Rule: preconnect to ~3 critical origins; dns-prefetch to others. Don’t preconnect to a dozen.

Q: When use preload?

A: Force the browser to fetch a specific resource immediately at high priority, before normal discovery. Use for resources that:

  1. Are critical to LCP but discovered late by the preload scanner.
  2. Are declared in CSS (the scanner can’t see them until CSS parses).

Examples:

<!-- The LCP hero image, before <picture>'s srcset selection -->
<link rel="preload" as="image" href="hero.avif" imagesrcset="hero-400.avif 400w, hero-800.avif 800w" imagesizes="100vw" />

<!-- Critical font, referenced in CSS -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/Inter.woff2" crossorigin />

<!-- A JS module loaded by another module -->
<link rel="modulepreload" href="/app/heavy.js" />

Critical: as attribute must match the resource type, otherwise the browser may refetch.

Don’t over-preload — every preloaded resource competes for bandwidth with the others. Preloading a 500 KB image plus three fonts will slow LCP, not improve it.

Q: modulepreload vs preload as="script"?

A: modulepreload is the ESM-aware version — it preloads and the browser parses the module’s dependency graph and preloads those too. Critical for ESM-heavy apps where one chunk imports many others.

<link rel="modulepreload" href="/chunks/dashboard.js" />

For non-module scripts, <link rel="preload" as="script" href="...">. For ESM, always modulepreload.

Modern bundlers (Vite) emit modulepreload links automatically for entry chunks and their imports.

Q: When use prefetch?

A: Low-priority fetch of resources for future navigations.

<link rel="prefetch" href="/dashboard/index.html" as="document" />
<link rel="prefetch" href="/chunks/dashboard.[hash].js" as="script" />

Browser fetches when idle, stores in HTTP cache. User clicks the link → loads from cache, near-instant.

Use cases:

  • Next likely route (Next.js Link prefetches viewport-visible routes by default).
  • Code-split chunks the user might need.
  • Hover-intent prefetch on nav items (onMouseEnter).

Don’t prefetch everything. On a cellular connection, prefetching a heavy chunk the user never clicks wastes their data plan. Be selective. Next.js does this only for in-viewport <Link>s on Wi-Fi-like connections.

Q: Priority Hints — fetchpriority attribute.

A: A separate but related mechanism on <img>, <link>, <script>, <iframe>:

<img src="hero.avif" fetchpriority="high" alt="..." />
<script src="defer-me.js" defer fetchpriority="low"></script>

Values: high, low, auto (default).

Common uses:

  • fetchpriority="high" on the LCP image — bumps it above other images and most scripts.
  • fetchpriority="low" on lazy/below-fold scripts or images you specifically want deprioritized.

Newer (Chrome 102+, Safari 17+) but well supported. The cheapest single LCP win when the LCP image is loading too slowly.

Q: Concrete LCP fix — image discovered late.

A: Page LCP is a hero image inside a React component, not in the initial HTML. The preload scanner can’t see it. Two fixes:

  1. Server-render the <img> into the HTML so the preload scanner finds it normally.
  2. Add a preload tag in <head> (server-rendered or static):
    <link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />

Both move the image’s start time from “after JS executes” to “immediately.”

Q: Speculation Rules API — the newer thing.

A: A Chrome-led API for prefetch and prerender of full pages.

<script type="speculationrules">
{
  "prerender": [
    { "urls": ["/dashboard"], "eagerness": "moderate" }
  ]
}
</script>

prerender actually pre-renders the page in a hidden frame; click → swap. The cost: extra work for pages the user may not visit; only do for high-confidence next steps.

Adoption is growing; Safari/Firefox catching up. The pattern is the future of “instant nav” but Next.js’ viewport-visibility prefetch still covers most needs.

Q: How does the preload scanner work and when does it fail?

A: When the HTML parser hits a <script>, it pauses parsing to run the script (unless async/defer). To keep the network busy, the browser also runs a preload scanner in parallel — a tokenizer that just finds resources to fetch (link/script/img/source URLs) and queues them.

The scanner finds:

  • <link> and <script> tags in HTML.
  • <img src> (and srcset).
  • <source> inside <picture> and <video>.

The scanner does not find:

  • Resources loaded by JavaScript (fetch(...), dynamic import()).
  • CSS background images (@import URLs, background-image properties).
  • Images set by client-side rendered components.

For these, resource hints in <head> make up the difference.

Gotchas / edge cases

  • Wrong as value on preload → browser refetches the resource. as="font" for fonts, as="image" for images, etc.
  • Missing crossorigin on font preload → browser may not actually reuse the preloaded response.
  • Preloading too many things competes with each other; LCP gets worse, not better. 3-5 critical preloads is a sane max.
  • preload of a resource you never use is a 100% waste — warned in DevTools console.
  • prefetch over cellular can burn user data. connection-aware hints (via navigator.connection) can gate.
  • dns-prefetch is a hint, not a guarantee — browsers may ignore on slow connections.
  • modulepreload not supported on older Safari — fall back to preload as="script" crossorigin for those.
  • <link rel="preconnect"> to a server that’s down still costs the round trip — preconnect only to origins you’re sure about.

What a senior is expected to say

  • “Resource hints are surgical, not free. dns-prefetch for cheap DNS warmup, preconnect for 3-5 critical origins, preload for things the preload scanner can’t find (LCP image, CSS-referenced fonts), prefetch for next-likely navigation, modulepreload for ESM critical chunks.”
  • fetchpriority='high' on the LCP image is the cheapest single LCP improvement when the image is loading slow.”
  • “Preload scanner finds tags in HTML but not resources loaded by JS or CSS. For those, hints in <head> are the bridge.”
  • “Over-preloading hurts — every preload competes for bandwidth. Limit to truly critical resources, measure with Lighthouse.”
  • “Speculation Rules API + prerender is the future for instant nav, but selective prefetch covers most needs today.”

Cross-references

Further reading