Fixing Unused Preload Warnings in Chrome DevTools
The console message is familiar and vague:
The resource https://cdn.example.com/assets/route-settings.4f2a91.js was preloaded
using link preload but not used within a few seconds from the window's load event.
Please make sure it has an appropriate `as` value and it is preloaded intentionally.
Two readings are common and both are wrong. It does not always mean the preload was wasted — sometimes the resource is used slightly later than the browser’s fixed timeout. And it is not always harmless — sometimes the resource was downloaded twice, doubling the bytes for a file you were trying to make faster.
The distinction is decidable in about a minute, and the prefetch and preload strategies for critical routes guide sets out the underlying model. This page is the diagnostic.
Root cause: a preload only satisfies an exactly matching request
A preload is a promise to the browser: this exact resource, fetched this exact way, will be needed by this navigation. The browser fetches it at high priority and holds it in a short-lived memory cache, waiting for a matching request to claim it.
“Matching” is stricter than the URL. The request destination (as), the credentials mode (crossorigin), and any integrity metadata must all agree. A mismatch on any of them means the later request does not claim the preloaded response, so the file is fetched again and the first copy is reported as unused.
Cause 1: attribute mismatch (real waste)
The most consequential case. Module scripts are always fetched in CORS mode, so a preload for one must carry crossorigin; a classic script on your own origin must not. Getting it backwards downloads the file twice.
<!-- Wrong: a module script preloaded without crossorigin — fetched twice -->
<link rel="preload" href="/assets/app.js" as="script">
<script type="module" src="/assets/app.js"></script>
<!-- Right: modulepreload states the destination and mode correctly -->
<link rel="modulepreload" href="/assets/app.js">
<script type="module" src="/assets/app.js"></script>modulepreload is the correct hint for anything loaded as an ES module, and it does more than match: it also lets the browser fetch and parse the module’s static dependency graph ahead of time, which removes the waterfall described in preventing waterfall requests with dynamic import maps.
Cause 2: preloading a future route (wrong hint)
A route chunk for a navigation that has not happened is not needed by the current navigation, so it warns by definition — and while it is downloading at high priority, it competes with the chunks rendering the page the user is actually looking at.
// route-hints.js — prefetch for future navigations, never preload
export function hintNextRoute(chunkUrl) {
const link = document.createElement('link');
// prefetch: lowest priority, idle time, no warning, no contention with
// the resources the current navigation actually needs.
link.rel = 'prefetch';
link.as = 'script';
link.href = chunkUrl;
document.head.appendChild(link);
}Reserve preload for resources the current view genuinely needs and the browser would otherwise discover late — a font referenced from a stylesheet, or an entry chunk imported dynamically from the bootstrap.
Cause 3: a hash that changed between hint and request
If the preload URL is emitted from a stale manifest — a cached HTML shell, a server-rendered page built against a previous release — it points at a filename the current runtime never requests. The preload downloads an obsolete file and the real one is fetched separately.
Serve HTML with no-store or short revalidation while chunks stay immutable, exactly as described in fixing ChunkLoadError after a new deploy. A preload pointing at a deleted hash is the mild version of the same defect.
Cause 4: conditional code paths (benign or real, depending)
A preload emitted unconditionally for a chunk only some sessions need — a locale, a polyfill bundle, an experiment variant — warns on every session that took the other branch. That is real waste, and the fix is to move the hint inside the same condition that decides the branch, as in conditionally importing polyfills at runtime.
Step-by-step verification
-
Filter the network panel by the warned URL. Two entries means the file downloaded twice — real waste. One entry means the resource was used, just later than the timeout.
-
Compare the request initiators. For two entries, check the
as,crossorigin, and priority columns. The mismatch will be visible in one of them. -
Switch module scripts to modulepreload. For anything loaded as an ES module, this resolves both the mismatch and the dependency-discovery delay.
-
Downgrade future-route hints to prefetch. Re-load the page and confirm the warning is gone and the priority column shows “Lowest” for those chunks.
-
Confirm the critical path did not regress. Compare first paint before and after. A hint you removed might have been doing real work.
-
Check that intentional preloads are still claimed. A remaining preload should appear once in the network panel, at high priority, with no warning.
Edge cases and gotchas
Framework-generated hints. Meta-frameworks emit preload and modulepreload tags automatically from their route manifests. A warning for a URL you did not write is usually generated, and the fix is in the framework’s route configuration rather than in your markup.
Warnings only in development. Development servers serve unbundled modules with different URLs than the production build, so both the warning and its absence can be artefacts of the dev server. Verify against a production build.
Service worker interception. A worker that responds to the later request from its own cache means the preloaded copy is never claimed, producing a warning even though nothing is wrong — see invalidating service worker cache after deploy.
Preloading fonts without crossorigin. Fonts are always fetched in CORS mode, so a font preload without the attribute is the most common non-script instance of the same mismatch.
FAQ
Is an unused preload warning ever safe to ignore?
Sometimes, but less often than teams assume. It is genuinely benign when the resource is used slightly after the warning window on a slow connection — the browser is measuring a fixed timeout, not correctness. It is not benign when the preload and the later request differ in URL or attributes, because then the resource really was downloaded twice. Distinguishing the two takes one look at the network panel: two entries for the same file means real waste.
Why does a preload with crossorigin warn when the file is on my own domain?
Because the crossorigin attribute changes the request mode, and a preload only satisfies a later request whose mode matches. A preload made in CORS mode cannot be reused by a same-origin request made without it, so the browser fetches the file a second time and reports the first as unused. Module scripts always fetch in CORS mode, so a preload for one must carry the attribute; a classic script on your own origin must not.
Should route chunks be preloaded or prefetched?
Prefetched, in nearly every case. Preload declares that a resource is needed for the current navigation and gives it high priority, which means it competes with the chunks actually rendering the current page. A chunk for a route the user has not navigated to yet is by definition not needed now, so prefetch — low priority, idle time, no warning — is the correct hint.
Related
- Prefetch and Preload Strategies for Critical Routes — the parent guide on when each hint is appropriate
- Prefetching on Hover and Viewport Intersection — issuing hints from real intent signals instead of guesses
- Preventing Waterfall Requests With Dynamic Import Maps — the discovery problem modulepreload solves
- Setting Up Route-Based Prefetching in Next.js — framework-generated hints and how to control them