Guide

Resource hints explained

Harbor Commerce's product listing page had a 2.4 s Largest Contentful Paint on mobile 4G — not because the hero image was oversized (they had already applied image optimization) but because the browser discovered critical assets too late. The hero WebP lived behind a CSS background rule; the Inter variable font loaded only after the stylesheet parsed; and the payment SDK origin required a cold DNS lookup plus TLS handshake before checkout could initialize. Lighthouse flagged all three as "network dependency chain" delays. The fix was not more code splitting — it was resource hints: declarative <link> tags that tell the browser to resolve, connect, or fetch resources before the parser would normally discover them. Adding preconnect for the font CDN and payment gateway, preload for the hero image and WOFF2 file, and fetchpriority="high" on the LCP element cut mobile LCP to 1.1 s without changing bundle size. Resource hints are the low-level scheduling layer beneath caching and bundling: they reduce connection latency, overlap critical fetches with HTML parsing, and deprioritize non-essential assets. This guide covers dns-prefetch, preconnect, prefetch, preload, modulepreload, fetchpriority, crossorigin requirements, a Harbor Commerce refactor walkthrough, a hint decision table, common mistakes, and a production checklist.

Resource hints vs caching and bundling

These optimizations operate at different layers and stack cleanly. HTTP caching (see HTTP caching explained) stores responses so repeat visits skip the network entirely. Bundling and code splitting reduce how many round trips a page needs by combining or deferring JavaScript. Resource hints affect the first visit: they start DNS, TCP, TLS, or full fetches earlier in the critical path so the browser does not idle while waiting to discover what it needs next.

Hints do not replace good cache headers or lean bundles. Preloading a 400 KB script you never execute wastes bandwidth; preconnecting to twelve third-party origins burns CPU on TLS handshakes. Use hints surgically on assets and origins the current navigation definitely needs, measured against Core Web Vitals field data rather than synthetic lab scores alone.

dns-prefetch

<link rel="dns-prefetch" href="//cdn.example.com"> resolves the hostname to an IP address only — no TCP or TLS. Cost is minimal (one UDP query, typically under 50 ms). Use it for third-party origins you will hit later in the page lifecycle but are not on the critical path right now: analytics, chat widgets, social embeds. On HTTPS pages, prefer preconnect for origins whose resources block rendering; reserve dns-prefetch for "maybe later" domains where a full connection would be premature.

dns-prefetch is a fallback when preconnect would be too aggressive or when you cannot know whether the user will trigger a third-party feature (e.g. a "Share" button that loads a social SDK on click). Browsers cap concurrent speculative DNS lookups; keep the list short.

preconnect

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> performs DNS + TCP + TLS (and optionally HTTP/2 negotiation) ahead of time. Place preconnect tags in <head> as early as possible — ideally before stylesheets that reference the origin. Each preconnect holds a connection open for roughly ten seconds; unused preconnects waste socket and memory resources on mobile devices.

The crossorigin attribute

Font files, images served with CORS, and ES modules require crossorigin on both the preconnect and the eventual resource fetch, or the browser opens a separate non-CORS connection and the hint is wasted. Same-origin assets and scripts loaded without CORS do not need the attribute. When in doubt, match the crossorigin mode of the downstream <link>, <img>, or <script type="module">.

Good preconnect candidates: font CDNs, your static asset subdomain, API gateways called during initial render, and payment or auth providers on checkout flows. Limit to three or four origins per page; more yields diminishing returns.

prefetch

<link rel="prefetch" href="/checkout.js" as="script"> fetches a resource at low priority for a likely future navigation. The browser downloads during idle time and stores the response in the HTTP cache for the next page load. prefetch is ideal for predictable user flows: cart to checkout, listing to detail, paginated "next" routes.

prefetch does not help the current page's LCP or INP. It can hurt if you prefetch assets the user never requests — especially on metered connections where browsers may ignore hints entirely. Pair prefetch with analytics on click-through rates; remove hints for flows with conversion below ~30%.

preload and modulepreload

<link rel="preload" href="/hero.webp" as="image" fetchpriority="high"> fetches a resource the current page needs with high priority, before the parser discovers it in CSS, JavaScript, or late body markup. The mandatory as attribute tells the browser what kind of resource to expect so it can apply correct content sniffing, CORS, and priority rules.

Common as values

  • as="image" — LCP heroes, above-the-fold SVGs
  • as="font" — critical WOFF2 faces (always with crossorigin and matching type)
  • as="style" — render-blocking CSS discovered late
  • as="script" — synchronous scripts in body
  • as="fetch" — JSON or GraphQL the app needs immediately

rel="modulepreload" is the ES-module variant: it preloads a module and its static import graph. Frameworks like Vite emit modulepreload links in production HTML for entry chunks. Manual modulepreload is useful when you split routes but know the user will hydrate a specific island on first paint.

Preload is powerful and easy to misuse. Every preloaded byte competes with other critical resources. Preload only assets that (a) appear on the critical path and (b) are discovered after a parser-blocking resource. If the hero is a plain <img src> in the first kilobyte of HTML, preload adds duplicate fetch risk — the browser may download the image twice. Prefer fixing discovery order first; add preload when the asset is hidden in CSS or JS.

fetchpriority and Priority Hints

The fetchpriority attribute ("high", "low", "auto") adjusts network scheduling for individual elements: <img fetchpriority="high">, <link rel="preload" fetchpriority="high">. Use high on the single LCP candidate; use low on below-the-fold images and non-critical iframes so they do not starve the hero.

fetchpriority complements lazy loading: an image with loading="lazy" and fetchpriority="high" is contradictory — pick one strategy per element. For font loading, combine preload with font-display: swap patterns documented in web font optimization rather than relying on priority alone.

Harbor Commerce storefront refactor

The listing page critical path looked like this before hints:

  1. HTML arrives; parser hits render-blocking CSS.
  2. CSS references background-image on .hero — browser starts hero fetch at ~800 ms.
  3. CSS @font-face triggers font fetch from fonts.gstatic.com after DNS + TLS (~600 ms overhead).
  4. Deferred checkout script loads Stripe.js origin on first interaction.

After measurement in WebPageTest and Chrome DevTools Network priority column, the team added to <head>:

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://js.stripe.com">
<link rel="preload" href="/assets/hero-catalog.webp" as="image" fetchpriority="high">
<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin>

They moved the hero from CSS background to a semantic <img> with explicit width/height (CLS fix) and kept the preload because the image still sat below an inline critical CSS block. They added fetchpriority="low" to 24 product thumbnails. Stripe preconnect saved ~350 ms on first "Add to cart" for users who opened the mini-cart overlay. Mobile LCP dropped from 2.4 s to 1.1 s; total page weight unchanged.

Hint decision table

Scenario Hint Notes
Third-party origin on critical path (fonts, API) preconnect Include crossorigin for fonts and CORS assets; max ~4 origins
Analytics or chat domain, maybe used later dns-prefetch Cheaper than preconnect; upgrade if metrics show early usage
LCP image discovered late (CSS/JS) preload + fetchpriority="high" Ensure URL matches final request exactly (query strings, CDN paths)
Critical WOFF2 above the fold preload as="font" crossorigin required; pair with subsetted WOFF2
Next-page route highly likely (checkout, page 2) prefetch Low priority; remove if click-through < 30%
ES module entry or heavy static import graph modulepreload Let bundler emit when possible; manual only for custom splits
Below-fold images and embeds fetchpriority="low" Combine with loading="lazy"; never on LCP candidate
Asset already in first HTML bytes None Fix markup order instead of duplicating with preload

Common pitfalls

  • Preload URL mismatch. A preload for /hero.webp when CSS requests /hero.webp?v=3 downloads the image twice. Match byte-for-byte including query strings and CDN path prefixes.
  • Missing crossorigin on fonts. Preconnect and preload without crossorigin on font files fail silently; text renders in fallback until a second fetch completes.
  • Preloading everything. More than two or three preloads per page often increases LCP by contending on HTTP/2 streams. Preload only provably late-discovered critical assets.
  • prefetch on metered connections. Mobile browsers deprioritize or skip prefetch; do not depend on it for core flows.
  • preconnect sprawl. Each unused TLS session costs memory; audit third-party preconnects after tag-manager changes.
  • Conflicting lazy and priority. loading="lazy" on the LCP image guarantees poor scores regardless of preload elsewhere.
  • Ignoring cache. Preload bypasses cache matching rules if as or credentials mode differ from the eventual fetch.

Production checklist

  • Capture field LCP element and its discovery time in DevTools Performance panel.
  • List every origin on the critical path; preconnect only those used within 3 s.
  • Verify font preloads include crossorigin, correct type, and matching URLs.
  • Confirm preload URLs exactly match downstream requests (Network tab filter).
  • Mark the single LCP candidate with fetchpriority="high".
  • Deprioritize below-fold media with fetchpriority="low" and lazy loading.
  • Add prefetch only for navigations with measured > 30% click-through.
  • Audit duplicate fetches after deploy (same resource, two initiators).
  • Re-test on slow 4G and mid-tier Android; hints behave differently than desktop fiber.
  • Document hints in your HTML template or SSR layer so marketing pages inherit them.

Key takeaways

  • Resource hints start DNS, connections, or fetches earlier than normal discovery — they optimize first visits, not repeat cache hits.
  • preconnect for critical third-party origins; preload for late-discovered assets on the current page; prefetch for likely next navigations.
  • crossorigin must match between hints and final fetches — especially for fonts and modules.
  • fetchpriority high/low fine-tunes scheduling within a page; use it on the LCP element and deprioritize the rest.
  • Measure duplicate fetches and real-user LCP before and after — over-hinting hurts more than no hints.

Related reading