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 SVGsas="font"— critical WOFF2 faces (always withcrossoriginand matchingtype)as="style"— render-blocking CSS discovered lateas="script"— synchronous scripts in bodyas="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:
- HTML arrives; parser hits render-blocking CSS.
- CSS references background-image on
.hero— browser starts hero fetch at ~800 ms. - CSS
@font-facetriggers font fetch from fonts.gstatic.com after DNS + TLS (~600 ms overhead). - 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.webpwhen CSS requests/hero.webp?v=3downloads the image twice. Match byte-for-byte including query strings and CDN path prefixes. - Missing crossorigin on fonts. Preconnect and preload without
crossoriginon 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
asor 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, correcttype, 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
- Core Web Vitals explained — LCP, INP, CLS thresholds and field measurement
- Web font optimization explained — WOFF2, font-display, preload patterns
- HTTP caching explained — Cache-Control, ETag, and repeat-visit performance
- Critical rendering path explained — parser, CSS blocking, and paint order