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?
| Preload | Used? | Needed for first render? | Discovered late? | Verdict |
|---|---|---|---|---|
inter-400.woff2 | all pages | yes (body text) | yes (in CSS) | keep |
inter-500, inter-600 | some pages | no | yes | remove |
inter-700.woff2 | all pages | only for headings below the fold on some templates | yes | remove |
jetbrains-mono-400 | pages with code (41%) | rarely above the fold | yes | remove |
syntax.css | pages with code | no | no (it is a <link> already) | remove |
hero-default.jpg | 0 pages (replaced by per-post heroes) | — | — | remove |
per-post hero.avif | its own page | yes (LCP element) | yes (CSS background) | keep, but only on that 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.
Measured Impact
The post template and the docs template, Lighthouse 12 mobile, median of five runs, preview deploy:
| Measure | Post before | Post after | Docs before | Docs after |
|---|---|---|---|---|
| Preload tags | 7 | 2 | 7 | 1 |
| Bytes fetched at high priority before LCP | 412 KB | 138 KB | 318 KB | 46 KB |
| LCP | 2.34 s | 1.71 s | 1.86 s | 1.22 s |
| FCP | 1.48 s | 1.19 s | 1.44 s | 1.12 s |
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 singlehreffetches one size; if the page's<img>picks another fromsrcset, the preload is wasted. Useimagesrcsetandimagesizes, or rely onfetchpriority. - 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
modulepreloadfor 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.
Related
- Parent: Resource Hints and Navigation Speed — when each hint is appropriate.
- Preloading Fonts Without Double Downloads — the one preload most sites should keep.
- Preconnect vs DNS-Prefetch on Static Sites — auditing connection hints the same way.
- Optimizing LCP on Astro with Priority Hints — fetchpriority as an alternative to preload.
- Writing a Performance Budget That Fails Builds — budgeting hints alongside bytes.