Your <link rel=preload> is downloading the file twice

javascript dev.to

Here's a bug that hides in plain sight: a preload hint that makes your site slower, in a way that looks perfectly correct in the HTML.

I found it last week while auditing a WordPress site before a host migration. The homepage was shipping 2.88 MB across 141 requests, and I was going through the waterfall looking for the usual suspects. Then I noticed the same filename twice.

The two lines

<link rel="preload" href="/wp-content/themes/x/library/js/plugins.min.js" as="script">
...
<script src="/wp-content/themes/x/library/js/plugins.min.js?ver=2.11.5"></script>
Enter fullscreen mode Exit fullscreen mode

Look at them for a second. Same path. Same file. The only difference is ?ver=2.11.5.

That difference is enough.

Why the browser doesn't care that it's "the same file"

The preload cache is keyed by the full request URL, query string included. It is not keyed by path, and it is certainly not keyed by content.

So the browser does exactly what you told it to:

  1. Sees the preload, fetches plugins.min.js at high priority.
  2. Parses on, hits the <script> tag, and looks for plugins.min.js?ver=2.11.5 in the preload cache.
  3. Doesn't find it — different URL — and fetches the whole thing again.

The preloaded copy is never used. It sits there and expires.

I verified the two responses were genuinely identical rather than assuming it:

$curl -s ".../plugins.min.js?ver=2.11.5" -o a.js
$curl -s ".../plugins.min.js"            -o b.js
$shasum -a 256 a.js b.js
94b4a2673e7c30d5b3dac15a9b55613ee106df3e91b0c65d052f94033ea7de74  a.js
94b4a2673e7c30d5b3dac15a9b55613ee106df3e91b0c65d052f94033ea7de74  b.js
Enter fullscreen mode Exit fullscreen mode

Byte-for-byte the same file. Downloaded twice.

This is worse than a wasted request

If it were merely wasteful, I wouldn't have bothered writing about it. The problem is when the waste happens.

rel=preload exists to say "this is important, get it now." The browser honours that: it fetches at high priority, immediately, during the exact window when the page is trying to paint. So the wasted copy isn't downloading during idle time at the end. It's competing for bandwidth with your LCP resource, at the front of the queue, by explicit request.

On the site I was looking at, both of the page's script preloads had this mismatch:

File Transferred (gzip) Downloaded
plugins.min.js 248 KB twice
init.min.js 28 KB twice

~276 KB of high-priority bandwidth, spent on bytes that were thrown away. On a mobile connection during the critical rendering window.

And the LCP element on that page? A CSS background image — which the preload scanner can't see at all, so it gets discovered late and queued behind all this. The one thing that actually needed the bandwidth was the one thing not getting it.

Why WordPress produces this so reliably

wp_enqueue_script() appends a cache-busting ?ver= to script URLs. That's standard, sensible behaviour.

The preload hints, though, usually come from somewhere else — a theme's performance option, or an optimization plugin generating resource hints. That code often builds the URL from the file path and forgets that the enqueue system is going to add a version string to the tag it's pairing with.

Both halves are individually reasonable. Together they silently double-fetch.

This is not a WordPress-only failure, to be clear. Any setup where preload hints and asset URLs are generated by different layers can drift: asset pipelines with content hashes, CDN rewrites, cache-busting middleware. WordPress just makes it especially easy.

Finding it in your own site

Don't rely on spotting it by eye — the two URLs are long and differ by a few characters near the end. Paste this in the console instead:

const preloads = [...document.querySelectorAll('link[rel=preload][as=script]')].map(l => l.href);
const scripts  = [...document.querySelectorAll('script[src]')].map(s => s.src);

preloads.forEach(p => {
  if (scripts.includes(p)) return;                       // exact match, fine
  const same = scripts.find(s => s.split('?')[0] === p.split('?')[0]);
  console.warn(same ? `MISMATCH\n  preload: ${p}\n  script : ${same}` : `UNUSED preload: ${p}`);
});
Enter fullscreen mode Exit fullscreen mode

MISMATCH means you're double-downloading. UNUSED means you're preloading something the page never requests at all — also worth deleting.

That's how I confirmed it on the site above: two preloads, both MISMATCH. Chrome may also surface a "preloaded using link preload but not used within a few seconds" warning in the Issues panel, but I wouldn't wait to notice it — run the check.

One caveat before you trust a clean result. That snippet reads the DOM, and on a page served by an optimization plugin the DOM is the optimizer's output, not what your server generated. I have watched the same URL hand back two different documents depending on whether it came from cache — in one case an entire inline <style> block was full on the origin and empty in the cached copy. So if the check comes back clean on a heavily cached site, confirm against the raw HTML before you believe it:

const url  = location.origin + location.pathname + '?nocache=' + Date.now();
const html = await fetch(url, { cache: 'reload' }).then(r => r.text());
console.log((html.match(/<link[^>]*rel=["']?preload["']?[^>]*>/gi) || []).length + ' preload tags at origin');
console.log(document.querySelectorAll('link[rel=preload]').length + ' preload tags in the DOM');
Enter fullscreen mode Exit fullscreen mode

If those two numbers disagree, audit the origin copy — that's the one your server is actually producing.

Same idea applies to as="style", as="font", and as="image". Fonts are the nastiest of the three, because a mismatched font preload also fails to warm the cache before the font is needed, so you pay twice and still get the swap.

The fix

Make the two URLs identical. Either add the same ?ver= to the preload href, or drop the version from the script tag — whichever your setup lets you control.

If you can't easily reach the code generating the hint, just delete the preload. A missing preload costs you a little discovery latency. A mismatched one costs you the full file size at the worst possible moment. Deleting is strictly better than leaving it broken.

The lesson

A preload tag is a promise that the browser will keep literally. It doesn't check whether you meant the same file. It checks whether you typed the same URL.

So when you audit resource hints, don't read them for intent. Diff them against what the page actually requests. Two URLs that a human would call "the same file" are, to the browser, simply two files.


I do this kind of technical detective work constantly. If you're tired of guessing why your site is slow, I packaged my exact audit checklist into SpeedKit for Claude Code.

Source: dev.to

arrow_back Back to Tutorials