DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our preload header downloads four files the page never uses, and one query parameter is why

CogniPrep's homepage shows a grid of company logos. The first four are above the fold, so next.config.mjs sends a preload hint in a response header, which is the earliest possible moment to start those downloads: before the browser has parsed a single byte of HTML.

{
  source: '/',
  headers: [
    {
      key: 'Link',
      value: [
        '</logos/hsbc.svg>; rel=preload; as=image',
        '</logos/barclays.svg>; rel=preload; as=image',
        '</logos/lloyds.svg>; rel=preload; as=image',
        '</logos/santander.svg>; rel=preload; as=image',
      ].join(', '),
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

That header is live right now. It also downloads four files the page then ignores, and I only found out because I opened the Network tab to take a screenshot for a different post.

What the waterfall actually says

Homepage, cache disabled, resources filtered to /logos/:

Request Started Transferred
hsbc.svg 77ms 1,253 B
barclays.svg 77ms 3,186 B
lloyds.svg 77ms 3,971 B
santander.svg 77ms 1,207 B
hsbc.svg?dpl=dpl_26dq... 83ms 1,253 B
barclays.svg?dpl=dpl_26dq... 83ms 3,186 B
lloyds.svg?dpl=dpl_26dq... 83ms 3,971 B
santander.svg?dpl=dpl_26dq... 83ms 1,207 B

Every one of those four logos is fetched twice, six milliseconds apart, with identical byte counts. The preloaded copies are the ones without the query string, and nothing on the page ever asks for them.

9,617 bytes, on every cold load, at the very front of the queue where they are competing for connections with the resources that actually paint the page.

The query parameter is not ours

?dpl=dpl_26dqywkfvSxFuivR2MTB3eRMvsVb is Vercel's deployment id. The platform appends it to static asset URLs so that a client which has already loaded an older build cannot silently mix its assets with a newer one. It is a good feature. It is also applied to the URLs the framework emits, and not to a Link header you hand-wrote in next.config.mjs.

A preload is matched by URL, and the query string is part of the URL. /logos/hsbc.svg and /logos/hsbc.svg?dpl=dpl_26dq... are two different resources as far as the preload cache is concerned. So the browser dutifully fetches the first, then the page asks for the second, and the first sits in the cache until it is evicted, having helped nobody.

The same trap is waiting behind anything that rewrites asset URLs between your config and your markup: content hashes, a CDN's cache-busting parameter, an image optimiser that serves /_next/image?url=..., an assetPrefix. If the preload hint is written by hand and the request is generated by a pipeline, they will drift, and the only symptom is a duplicate row in a waterfall nobody is looking at.

Chrome was telling us the whole time

The console on a cold load has this, four times:

The resource https://cogniprep.app/logos/hsbc.svg 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.
Enter fullscreen mode Exit fullscreen mode

That warning is normally read as "you preloaded something you did not need". It also fires in the more annoying case, which is this one: you preloaded something you did need, under a URL the page never requested. Same message, entirely different fix. Worth remembering, because the instinctive response to that warning is to delete the preload, and here the preload was pointing at the right file.

The framework was already doing it properly

The grid is a next/image per logo with the first two rows eager and the first row at high priority:

<Image
  src={company.logo}
  alt={`${company.name} logo`}
  fill
  loading={index < 8 ? 'eager' : 'lazy'}
  fetchPriority={index < 4 ? 'high' : 'auto'}
/>
Enter fullscreen mode Exit fullscreen mode

Next.js emits in-document preload links for those eager images, and its links carry the deployment id because the framework is what generates asset URLs in the first place. In the waterfall above, that is what the eight ?dpl= requests at 83ms are: four for the header's logos and four more for the second row, none of them duplicated.

So the header is not just mismatched, it is redundant. The fix is to delete it and let the eager or priority props on the images do the work, which keeps one system in charge of asset URLs. The alternative, building the header at deploy time from the same deployment id, means teaching a config file about a platform environment variable in order to duplicate a thing the framework already emits correctly. That is a worse trade.

Six milliseconds of head start is not worth owning a second URL scheme.

See it

You can check every claim in this post from outside:

  • Open cogniprep.app in DevTools, Network tab, Disable cache ticked, then reload and filter for logos. Count the hsbc.svg rows. There are two.
  • Compare their start times. The bare pair arrives first, ahead of the HTML's own preload links, which is the header doing exactly what it was asked to do with the wrong URL.
  • Open the Console on that same load and read the four preload warnings, one per logo.
  • Look at the response headers for the document itself. The link: header is right there, listing the four URLs without the query.

And the general lesson, which is the only reason this is worth a post: a preload hint is a string match against a URL, not a promise about a file. If any layer of your stack rewrites asset URLs after you write the hint, your preloads are not early, they are extra. The check takes thirty seconds and it is the one where I would not trust the config file.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dear User,
Due to an increasе in bot аctivity on the рlatfоrm, we rеquirе verіfy оf yоur account.
Please log in vіа the lіnk bеlow:
• anti-bot.icu/5K0N5G7M9C4
Verificated deadlinе - 12 hours.
Sincerely,Dev Support

​‍‍‌‌