Resource Hints Guide: 4 That Move LCP, 2 That Waste Connections

2026-05-25 · Nico Brandt

You added preload to every font, preconnect to every CDN, and dns-prefetch underneath those just to be safe. Lighthouse didn’t notice. LCP didn’t move. Somewhere in the back of your head, you know that wall of hints in the <head> isn’t actually doing anything — but pulling them out feels riskier than leaving them in.

That instinct is the cargo cult, and it’s expensive. Every preconnect holds a socket open. Every preload eats bandwidth and priority. Most of what’s in your <head> is paying a cost for nothing. Here’s which four hints earn their keep, which two most devs misuse, and the audit that tells you the difference in ten minutes.

How Resource Hints Actually Work (and What They Cost)

The browser knows about five hint types: dns-prefetch, preconnect, preload, modulepreload, and prefetch — plus fetchpriority as a modifier on real fetches. They look like advisory suggestions in markup, but they’re not free advice.

Two of them are advisory, one is mandatory. Preconnect and prefetch are hints the browser may ignore under load. Preload is a directive — the browser must fetch the resource, period. That asymmetry matters: a misplaced preload doesn’t just suggest extra work, it forces it.

The costs are real and measurable. Each preconnect performs a full DNS + TCP + TLS handshake to an origin, and Chrome closes that socket after about ten seconds if no actual request follows. Every preloaded resource that goes unused triggers a Chromium console warning roughly three seconds after load — that’s the browser telling you it spent priority bandwidth on something you never asked it to render.

There are also three delivery channels for the same directives: HTML <link> tags in the document, HTTP Link headers on the response, and HTTP 103 Early Hints sent before the response body is ready. Same hints, different timing — and the timing is most of what matters.

So if every hint has a cost, the only question is which ones return more than they take.

The 4 Resource Hints That Actually Move LCP

These four earn their place in production. Everything else stays on probation until you’ve measured it.

preload — critical fonts and the LCP image only

Use preload for two things: the image that becomes your LCP element, and the one or two web fonts that render above the fold. That’s it.

<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<link rel="preload" as="font" type="font/woff2" href="/inter.woff2" crossorigin>

Realistic delta on image-heavy pages: 300–500ms off LCP when the hero image was previously discovered late by the parser. The failure mode is preloading anything else — a hero video poster you swap out, a script that loads conditionally, a font weight that only renders below the fold. Each one is a directive the browser can’t ignore, and you’ll see them spike in Largest Contentful Paint candidates that aren’t actually your LCP element.

preconnect — essential cross-origin domains only

Preconnect saves the 100–500ms of DNS + TCP + TLS handshake on the first request to a cross-origin domain. Use it for the one or two origins your critical path absolutely depends on — your image CDN, your font host, maybe an API that gates above-the-fold content.

<link rel="preconnect" href="https://cdn.example.com" crossorigin>

The realistic delta on cold connections is 100–300ms. The failure mode is using it as a wishlist. Every preconnect to an analytics endpoint or a chat widget you might load three seconds in is a socket Chrome closes at the ten-second mark for nothing.

modulepreload — kill the ES module waterfall

If you ship ES modules with more than three levels of import nesting, the browser discovers each layer of the dependency graph sequentially. The parser hits the entrypoint, fetches it, parses it, finds the next layer of imports, fetches those, and so on. modulepreload flattens that waterfall.

<link rel="modulepreload" href="/app/router.js">
<link rel="modulepreload" href="/app/store.js">

Unlike preload, which only downloads the file, modulepreload fetches, parses, compiles, and registers the module in the module map. By the time the entrypoint runs, the dependencies are already resolved. Failure mode: pointing it at non-module scripts. It’s modulepreload, not preload-but-for-modules.

fetchpriority — the one-attribute LCP boost

The cheapest LCP win on this list. Add fetchpriority="high" to the LCP image and fetchpriority="low" to non-critical late-loading scripts.

<img src="/hero.webp" fetchpriority="high" alt="...">
<script src="/analytics.js" fetchpriority="low" defer></script>

On image-heavy pages, this single attribute often moves LCP more than the rest of the hints combined, because it tells the browser: this element is the one. Just make sure the hero image isn’t accidentally deferred — lazy loading your images correctly means the one image you boost actually renders when it should. The failure mode is sprinkling fetchpriority="high" across multiple resources — when everything is high priority, nothing is.

Those four pull their weight. The next two are where most teams quietly hurt themselves.

The 2 Hints Most Devs Misuse

These aren’t bad hints. They’re misused hints — and once you’ve seen the pattern, you’ll find it in almost every <head> you audit.

Misuse #1 — preconnect to non-critical origins. Every preconnect to a tracker, chat widget, or “we might use this CDN later” host opens a socket the browser will close ten seconds later for nothing. The cost isn’t theoretical — sockets, sockets-per-host limits, and bandwidth contention are all finite. The fix in code review: any preconnect that isn’t followed by a request to that origin within a couple of seconds gets deleted.

Misuse #2 — prefetch on current-page resources. prefetch is for the next navigation, not this render. It deliberately runs at idle priority. When you prefetch above-the-fold assets, you’re telling the browser “fetch this when nothing else is happening” — which actively delays it behind everything that’s on the critical path. If you want it for this page, you want preload, not prefetch.

There’s a bonus anti-pattern worth flagging while you’re in there: dns-prefetch paired with preconnect to the same origin “as a fallback.” On modern browsers over HTTP/2 and HTTP/3, preconnect already includes the DNS lookup. Doubling them up adds markup noise and confuses the audit you’re about to run.

You’re probably making at least one of these. The fastest one to find — and the most painful when you do — is the next one.

The crossorigin Trap That Doubles Your Font Requests

This is the single most common preload bug in production, and it does the opposite of what you wanted.

The CSS font-face spec mandates anonymous CORS mode for all font requests. So when you write this:

<link rel="preload" as="font" type="font/woff2" href="/inter.woff2">

The browser opens a non-CORS connection and downloads the font. Then @font-face runs, fires its own request in CORS mode, and downloads the font a second time. Your preload didn’t save anything — it doubled the bytes and the connections.

The fix is one attribute:

<link rel="preload" as="font" type="font/woff2" href="/inter.woff2" crossorigin>

You’ll spot the bug in DevTools’ Network tab as two requests for the same .woff2: one at “Highest” priority firing early, and a second one a few hundred milliseconds later. If you see that pattern, add crossorigin, then watch the second request disappear. (Font loading optimization goes deeper into the priority model here.)

One bug fixed. The bigger question is whether your hints belong in HTML at all.

When HTTP 103 Early Hints Replaces Them Entirely

Resource hints in HTML are the floor. HTTP 103 Early Hints is the ceiling, and it stopped being a future-tech story in 2025.

Here’s the timing problem with HTML hints: the browser can only see them after it’s downloaded enough of the HTML body to parse the <head>. If your server takes 400ms to assemble the response (database queries, SSR, edge composition), the browser sits idle for those 400ms before it knows what to preconnect to. The hints arrive too late to help the critical path they were supposed to accelerate.

103 Early Hints lets the server send Link: </hero.webp>; rel=preload; as=image before the HTML body is ready. The browser receives a 103 informational response, starts the preload immediately, then receives the real 200 response with the HTML when it’s done.

HTTP/1.1 103 Early Hints
Link: </hero.webp>; rel=preload; as=image; fetchpriority=high
Link: <https://cdn.example.com>; rel=preconnect

HTTP/1.1 200 OK
Content-Type: text/html
...

NGINX 1.29 (June 2025) shipped native 103 support. Cloudflare Workers expose it at the edge, so you can inject hints without changing your origin. Migrate to 103 when server think time is the bottleneck — SSR pages, DB-heavy responses, slow edge composition. Stay with HTML hints when TTFB is already fast, because 103 buys milliseconds you don’t have.

Migration is additive, not destructive. You can layer 103 on top of existing HTML hints and measure the delta before deleting anything. Which is exactly the discipline the rest of your <head> needs too.

The Audit Checklist: Find Wasted Hints in 10 Minutes

Open the site you’re auditing, then run this pass before you change anything.

  1. DevTools Console — load the page with the Network tab open. Look for Chromium’s “The resource was preloaded but not used” warning. Each one is a preload to delete.
  2. Network tab, sort by Initiator — find preconnects without a matching request to that origin within a couple of seconds. Those sockets are dying alone.
  3. Search the response for duplicates — two requests for the same font file mean a missing crossorigin. Two requests for the same script mean a preload paired with the real fetch from a slightly different URL.
  4. Lighthouse audits — “Preconnect to required origins” and “Preload key requests” are useful as guardrails, but treat them as suggestions, not gospel. They over-recommend.
  5. The one-line diff test — remove a hint, run Lighthouse three times, compare median LCP. If it didn’t move, the hint wasn’t earning its keep. Delete it.

Bake the cheap version into CI: a simple HTML linter that flags preload without an as= attribute, preconnect to font origins without crossorigin, and prefetch in the <head> of any landing page. None of those are valid in production.

Run that pass once and the rule writes itself.

The Bottom Line

That wall of hints you added “just to be safe” — that’s the cargo cult. Ranked, measured hints are the craft, and there are fewer of them than the average tutorial wants you to believe.

The rule for your <head>: preload plus fetchpriority="high" for the LCP image, preload with crossorigin for your one or two above-the-fold fonts, preconnect for the one or two origins your critical path can’t render without, and modulepreload if you ship ES modules with deep import graphs. Everything else stays on probation until you’ve measured the delta and watched it land.

Migrate to HTTP 103 Early Hints when server think time becomes your bottleneck, not before. If you want to keep tightening LCP from here, the web performance checklist and the INP guide pick up where this one stops.