Auditing Unused Preloads

<link rel="preload"> is the sharpest tool in the resource-hint kit and the one most often misused. A preload says "fetch this now, at high priority, before you discover you need it." Used for the one late-discovered critical resource on a page — usually a web font or a CSS background hero — it can take hundreds of milliseconds off LCP. Used for anything else, it takes bandwidth away from the resources that actually decide LCP, and on a static site with shared layouts, one bad preload is repeated on every page.

This guide audits every preload on a 600-page Hugo site, classifies each one, removes the harmful ones and adds a CI check so they do not come back. It is part of Resource Hints and Navigation Speed.

Prerequisites

  • The built site's HTML output (public/ for Hugo, dist/ for Astro, _site/ for Eleventy).
  • Chrome DevTools, for confirming the effect on individual templates.
  • Lighthouse or Lighthouse CI to measure LCP before and after — see Setting Up Lighthouse CI for a Static Site.

Step 1: Inventory Every Preload in the Build

Because a static site's HTML is all on disk, the inventory is a script, not a crawl. Collect every preload and modulepreload tag, grouped by template:

# count preload hrefs across all built pages
grep -rhoE '<link[^>]+rel="(module)?preload"[^>]*>' public --include=index.html \
  | grep -oE 'href="[^"]+"' | sort | uniq -c | sort -rn | head -20
    612 href="/fonts/inter-400.woff2"
    612 href="/fonts/inter-500.woff2"
    612 href="/fonts/inter-600.woff2"
    612 href="/fonts/inter-700.woff2"
    612 href="/fonts/jetbrains-mono-400.woff2"
    612 href="/css/syntax.css"
    604 href="/img/hero-default.jpg"
     38 href="/img/posts/…/hero.avif"

Every page preloaded five font files, a syntax-highlighting stylesheet and a default hero image. The theme had added them; nobody on the team had written them.

Step 2: Classify Each Preload

For each distinct preload, answer three questions: is it used on this template at all, is it needed for first render, and is it discovered late without the hint?

PreloadUsed?Needed for first render?Discovered late?Verdict
inter-400.woff2all pagesyes (body text)yes (in CSS)keep
inter-500, inter-600some pagesnoyesremove
inter-700.woff2all pagesonly for headings below the fold on some templatesyesremove
jetbrains-mono-400pages with code (41%)rarely above the foldyesremove
syntax.csspages with codenono (it is a <link> already)remove
hero-default.jpg0 pages (replaced by per-post heroes)remove
per-post hero.avifits own pageyes (LCP element)yes (CSS background)keep, but only on that page
Decision tree for each preload Three questions in sequence. Is the resource used on this page? If not, remove it: it is wasted bytes. Is it needed for first render? If not, remove it: it competes with the LCP resource. Is it discovered late without the hint? If not, remove it: the hint is redundant. Only a resource that passes all three keeps its preload. Three questions; a preload must pass all of them used on this page? needed for first render? discovered late? remove wasted bytes remove competes with LCP remove redundant hint keep the preload yes yes no no no yes
Of eight distinct preloads on the site, two survived: the body font on every page, and each post's own hero on its own page.

Step 3: Fix at the Template Level

Remove the theme's blanket preloads by overriding its head partial, then emit the two legitimate ones precisely:

{{/* layouts/partials/head/preloads.html */}}
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
{{ with .Params.hero }}
  {{ with $.Resources.GetMatch . }}
    {{ $img := .Resize "1200x webp q78" }}
    <link rel="preload" as="image" href="{{ $img.RelPermalink }}" fetchpriority="high">
  {{ end }}
{{ end }}

The per-post hero preload now exists only on pages that have a hero, points at the exact processed file the page renders, and carries fetchpriority="high" so the browser treats it as the LCP candidate it is. For heroes rendered as <img> elements rather than CSS backgrounds, drop the preload entirely and put fetchpriority="high" on the <img> — the image is already discoverable in the HTML, as shown in Reducing LCP from Hero Images on Static Sites.

Font preloads need the crossorigin attribute even on the same origin, because fonts are fetched in CORS mode; without it the preload is not matched and the font downloads twice. That detail is covered in Preloading Fonts Without Double Downloads.

Step 4: Guard It in CI

A theme update reintroduced two font preloads three weeks after the clean-up. A build check now counts preloads per page and verifies each preloaded URL is referenced elsewhere in the same page:

// scripts/check-preloads.mjs
import { globSync, readFileSync } from 'node:fs';
const MAX = 2;
let bad = 0;
for (const f of globSync('public/**/index.html')) {
  const html = readFileSync(f, 'utf8');
  const pre = [...html.matchAll(/<link[^>]+rel="preload"[^>]+href="([^"]+)"/g)].map((m) => m[1]);
  if (pre.length > MAX) { console.log(`${f}: ${pre.length} preloads`); bad++; }
  const body = html.replace(/<link[^>]+rel="preload"[^>]*>/g, '');
  for (const href of pre) {
    const name = href.split('/').pop();
    if (!body.includes(name)) { console.log(`${f}: preload ${href} not referenced`); bad++; }
  }
}
process.exit(bad ? 1 : 0);

Checking that the filename appears elsewhere in the page catches preloads for resources the page never uses — though for fonts referenced only from CSS, add the stylesheet's font list to the check, or exempt the known font file by name.

Preload count over time with and without the CI check A step line of preload tags per page. It starts at 7, drops to 2 after the clean-up, and without a check rises back to 4 three weeks later when a theme update adds two font preloads. With the check, the theme update's pull request fails and the count stays at 2. Preloads per page: clean-up, then a theme update 0 4 8 7 (theme defaults) 2 after clean-up no check: back to 4 with check: PR fails, stays 2 theme update six weeks of builds on the 600-page Hugo site
Without a guard, clean-ups decay; the check turned the theme update into a failed pull request instead of a slower site.

Measured Impact

The post template and the docs template, Lighthouse 12 mobile, median of five runs, preview deploy:

MeasurePost beforePost afterDocs beforeDocs after
Preload tags7271
Bytes fetched at high priority before LCP412 KB138 KB318 KB46 KB
LCP2.34 s1.71 s1.86 s1.22 s
FCP1.48 s1.19 s1.44 s1.12 s
High-priority bytes before LCP and the resulting LCP Two paired comparisons. On the post template, high-priority bytes before LCP fell from 412 to 138 kilobytes and LCP fell from 2.34 to 1.71 seconds. On the docs template, bytes fell from 318 to 46 kilobytes and LCP fell from 1.86 to 1.22 seconds. Fewer bytes competing, earlier LCP Post 412 KB · LCP 2.34 s 138 KB · LCP 1.71 s Docs 318 KB · LCP 1.86 s 46 KB · LCP 1.22 s before: theme's blanket preloads after: one font + the page's own hero Lighthouse 12 mobile, simulated 4G, median of 5; bar length = KB fetched at high priority before LCP
Removing preloads made both templates faster: the LCP resource no longer shared the connection with six things it did not need.

In the field, over the following four weeks, the site's mobile LCP p75 from CrUX moved from 2.3 s to 1.8 s. It is the rare performance fix that consists entirely of deleting code.

Reading the Evidence in DevTools

Before deleting a preload, it is worth confirming its effect on one page so the change can be explained in review. Three views make the case. The Console lists "The resource … was preloaded using link preload but not used within a few seconds" for truly unused preloads — hero-default.jpg produced that warning on every page. The Network panel, sorted by start time with the Priority column shown, reveals used-but-unnecessary preloads: on the post template, four font files and a stylesheet started at Highest priority in the same 50 ms window as the hero image, and the hero's download stretched from 280 to 610 ms as they shared bandwidth. And the Performance panel's LCP breakdown shows the "resource load duration" subpart shrinking once the competition is removed — from 560 ms to 230 ms on this template, which accounts for most of the LCP gain. The same breakdown is walked through in Measuring LCP Subparts with DevTools.

Pitfalls & Rollback

  • Deleting the one preload that matters. Keep the late-discovered critical font; removing it can reintroduce invisible text or a font swap shift. Measure after every removal.
  • Preloading responsive images without imagesrcset. A preload with a single href fetches one size; if the page's <img> picks another from srcset, the preload is wasted. Use imagesrcset and imagesizes, or rely on fetchpriority.
  • Auditing one page only. Preloads come from shared partials, so an audit of the homepage alone misses template-specific ones. Scan every built page, grouped by template.
  • Framework preloads you did not write. Bundlers emit modulepreload for route chunks. Those are usually right; audit them only if a template preloads chunks it never executes.
  • Blanket fixes in the layout. A preload that is right for one template is wrong for another. Emit hints per template from data the page already has.
  • Rollback: preloads are single tags in partials. Restoring one is a one-line revert, and the CI check's allowance can be raised per template if a new, justified preload is needed.

Conclusion

Preloads are cheap to add and easy to forget, and themes add them for you. Auditing the built HTML found seven preloads on every page of a 600-page site; two were justified, five competed with the LCP image or fetched files the page never used. Removing them cut high-priority bytes before LCP by two thirds and LCP by 0.6 seconds on both templates tested, and a ten-line CI check has kept them from creeping back since.

FAQ

What counts as an unused preload?

A preloaded resource that the page does not use within about three seconds of the load event. Chrome logs a console warning for each one, and the resource was downloaded at high priority for nothing.

Can a preload be used but still harmful?

Yes. A preload for a resource that is needed but not critical, such as a below-the-fold image or a second font weight, still competes with the LCP resource for bandwidth. Used-but-unnecessary preloads delayed LCP more than unused ones on the site in this guide.

Where do unwanted preloads come from?

Most often from themes and plugins that preload every font weight or a hero image on every template, from framework defaults that preload route chunks, and from hand-written hints that outlived the asset they pointed to.

How do I stop preloads creeping back?

Parse the built HTML in CI, count preload tags per template, and fail the build when a template exceeds its allowance or preloads a resource the page does not reference. It takes a few seconds and catches theme updates immediately.