Frontend / Performance / 08_resource_hints.md

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

Updated 5 interview angles 7 min read source
On this page13
  1. When use dns-prefetch?
  2. preconnect vs dns-prefetch?
  3. When use preload?
  4. modulepreload vs preload as="script"?
  5. When use prefetch?
  6. Priority Hints — fetchpriority attribute.
  7. Concrete LCP fix — image discovered late.
  8. Speculation Rules API — the newer thing.
  9. How does the preload scanner work and when does it fail?
  10. Gotchas
  11. Related
  12. Further reading
  13. Interview angle

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

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.

When use dns-prefetch?

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

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

preconnect vs dns-prefetch?

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

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

When use preload?

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:

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

modulepreload vs preload as="script"?

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.

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

When use prefetch?

Low-priority fetch of resources for future navigations.

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

Priority Hints — fetchpriority attribute.

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

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

Concrete LCP fix — image discovered late.

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):
    html
    <link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />

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

Speculation Rules API — the newer thing.

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

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

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

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

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

Further reading

Interview angle 5

  • “What are the resource hints and when does each apply?” - dns-prefetch for a cheap DNS warm-up, preconnect for the handful of origins on the critical path, preload for something the preload scanner cannot find, prefetch for the next likely navigation, and modulepreload for critical ES module chunks. They are surgical instruments, not defaults.
  • “What is the preload scanner and why does it matter?” - a lightweight parser that races ahead of the main parser finding resources to fetch early. It reads HTML, so it finds your <img> and <script> tags - and it cannot see anything referenced from CSS or created by JavaScript. That gap is exactly what preload is for: a font referenced by @font-face or a hero image set as a CSS background.
  • “What is the single cheapest LCP fix?” - fetchpriority="high" on the LCP image, when the image is being discovered or downloaded too late. It costs one attribute and reprioritises the browser’s connection budget.
  • “Can you over-preload?” - easily, and it is the common mistake. Every preload competes for the same bandwidth, so preloading ten things delays the one that mattered and the page gets slower. Keep it to genuinely critical resources and verify in a trace.
  • “How many origins would you preconnect to?” - three to five. Each one costs a DNS lookup, a TCP handshake and a TLS negotiation held open, so preconnecting to every third party on the page spends the connection budget on things that are not on the critical path.