How to Fix "The resource was preloaded but not used" Warnings
This walkthrough sits under Resource Hints & Early Hints within Advanced Caching Strategies & CDN Architecture. The console message is easy to dismiss and expensive to ignore: it means the browser fetched a file at high priority, on the critical path, and then never used it — either because nothing needed it, or because what needed it made a different request and downloaded the file a second time.
Both outcomes waste the bandwidth you were trying to save, and the second one is worse because the page looks correct.
Rapid Diagnosis: Duplicate, or Genuinely Unused?
- Filter the Network panel by the filename. Two entries for the same URL means an attribute mismatch — the preload and the real request are not the same request. One entry means the resource is genuinely never needed.
- Compare the two rows' details. Check
Priority, theAcceptheader, and whether the request was sent with credentials. The difference between the rows names your bug. - Check the initiator of the second request. If it is a stylesheet or a script, that is the consumer whose request shape you must match.
- Time the warning. Chrome fires it a few seconds after load. A resource used later — on interaction, for instance — will warn even though the preload was arguably reasonable; that is a signal to use
prefetchinstead.
Root Cause Analysis
1. Missing crossorigin on a font. Fonts are always fetched in anonymous CORS mode. A preload without the attribute performs a credentialed fetch, which the browser treats as a distinct resource, so the CSS-initiated request downloads the file again. This is by far the most common cause.
2. A wrong or absent as value. as determines the request's priority band and its Accept header. as="fetch" on a stylesheet requests it as an opaque fetch, at a different priority, and the CSS parser will not reuse it.
3. A stale URL after a build. Content-hashed filenames change on every deploy. A hint written into a template or a server config by hand keeps pointing at last week's file, producing a high-priority 404 while the real asset loads normally.
4. A media attribute that excludes the current viewport. Preloading a stylesheet with media="(min-width: 900px)" on a phone fetches nothing usable; the file is downloaded and then never matched.
5. The resource is only needed later. A preload for something used on interaction is the wrong hint entirely — prefetch exists for that, and it will not compete with the critical path.
Step-by-Step Resolution
1. Match the request shape exactly for fonts.
<!-- Wrong: credentialed fetch, downloads twice. -->
<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2">
<!-- Right: anonymous, same shape as the CSS-initiated request. -->
<link rel="preload" href="/fonts/inter-latin.woff2" as="font"
type="font/woff2" crossorigin>
<!-- trade-off: crossorigin is required even same-origin. Adding it to a
resource that is NOT fetched anonymously — a credentialed API response,
for instance — creates the mismatch in the other direction. -->
Expected outcome: one request instead of two; the warning disappears and the font is used from the preload.
2. Use the correct as for the consumer. The value must describe what the resource is, not how you think of it.
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/route-home.js" as="script">
<link rel="modulepreload" href="/js/route-home.js"> <!-- for ES modules -->
<link rel="preload" href="/hero.avif" as="image" type="image/avif">
<!-- trade-off: modulepreload also parses the module and its import graph,
which costs main-thread time up front. For a large graph that can delay
other work — preload the entry and let the rest resolve normally. -->
Expected outcome: the preloaded response lands in the right cache bucket at the right priority, and the consumer reuses it.
3. Generate hints from the build manifest. Any hint naming a hashed asset must come from the same source of truth as the asset.
// vite/webpack plugin sketch: emit head hints from the real emitted filenames.
function emitHints(bundle) {
const entry = Object.values(bundle).find((c) => c.isEntry);
const font = Object.keys(bundle).find((f) => f.endsWith('.woff2'));
return [
`<link rel="modulepreload" href="/${entry.fileName}">`,
`<link rel="preload" href="/${font}" as="font" type="font/woff2" crossorigin>`
].join('\n');
}
// trade-off: generated hints are correct but easy to over-generate — a plugin
// that hints every emitted font will preload five faces. Filter to the ones
// the above-the-fold design actually uses.
Expected outcome: hints can never point at a file the deploy does not contain.
4. Downgrade or delete hints that do not qualify. If the resource is used on interaction, switch to prefetch. If it is used on a viewport the visitor does not have, move the hint behind the same media query. If nothing uses it, delete it — a removed hint is a guaranteed improvement.
Verification
Reload with an empty cache and confirm the Network panel shows exactly one request per preloaded URL, at the priority you expect, with the console clean. Then check the thing the warning was protecting you from: compare the LCP timestamp before and after. Removing a duplicated high-priority download often improves LCP by more than the preload ever did.
Automate the check so it cannot regress:
// Playwright: fail the build if any preload warning appears during a load.
const warnings = [];
page.on('console', (m) => {
if (/preloaded using link preload but not used/i.test(m.text())) {
warnings.push(m.text());
}
});
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(4000); // the warning fires a few seconds in
if (warnings.length) throw new Error(warnings.join('\n'));
// trade-off: the fixed wait makes the check slow and slightly flaky. Scope it
// to a handful of representative routes in a nightly job rather than running
// it on every pull request.
Keep this in the same job that runs your other budget assertions, described in Resource Hints & Early Hints. A hint that silently stops matching after a refactor is the single most common way this regresses.
FAQ
Is the warning ever safe to ignore?
Only in one case: a resource deliberately preloaded for an interaction that usually happens after the warning's timeout. Even then, prefetch expresses that intent correctly and does not compete with the critical path, so the better fix is to change the hint. Every other instance of the warning means you either paid for a file twice or paid for one you never used.
Why does removing the preload sometimes make the page faster?
Because the preload was competing with resources that mattered more. A duplicated high-priority font download consumes bandwidth that would otherwise have gone to the LCP image or the critical stylesheet. Deleting a wrong hint is one of the few performance changes that is guaranteed not to make things worse.
Does the warning appear for Link header hints too?
Yes — the source of the hint does not matter, only whether the fetched resource is used. Header-delivered hints are actually more prone to this, because they are frequently configured per location block and therefore apply to routes that do not need the asset. Scope header hints per route, and generate them from the build.
Why does the warning sometimes appear only on mobile?
Because a media attribute or a responsive layout excluded the resource at that viewport. A stylesheet preloaded for a wide-screen layout is downloaded on a phone and then never matched, which is exactly the condition the warning describes. Reproduce it by emulating the narrow viewport before loading, then either scope the hint with the same media query as the resource or drop it for that breakpoint.
Can a service worker cause a false positive?
It can. If a worker intercepts the real request and serves it from a cache, the browser may not associate that response with the preloaded one, so the preload looks unused even though the resource was needed. Confirm by testing with the worker unregistered: if the warning disappears, the hint is fine and the interaction is the artefact. In that case, either let the preloaded response satisfy the worker's fetch or drop the hint, since the cache already made it redundant.
Does the same warning exist for modulepreload?
Yes, and it is worth taking more seriously, because modulepreload also parses the module and its import graph. An unused one wastes main-thread time as well as bandwidth. The usual cause is preloading an entry that the route no longer imports after a refactor — another argument for generating hints from the build manifest rather than maintaining them by hand.
Related
- Resource Hints & Early Hints — when a preload is justified in the first place.
- Preload vs preconnect: when to use each — pick the hint that matches what you actually know.
- Font Loading & Text Rendering Performance — the resource class where this mistake is most frequent and most costly.