dotvitals

Loading the list of pages…

to moveEnter to openEsc to close

Static files are re-downloaded because they are barely cached

LowConfirmedQuick winperf.assets.static-assets-short-cache

What this check looks for

‹count› static file(s) this page loads — scripts, stylesheets, fonts, images — are served with a cache lifetime under an hour or none at all, so returning visitors fetch them again.

Why it matters

A returning visitor should download almost nothing. Short cache lifetimes on files that never change turn every repeat visit into a first visit, for no benefit at all.

When the check passes, your report says: “Static files are cached long enough to survive a repeat visit”.

What it costs your score

When this check fails it removes 6 points from your Performance score, before the status, confidence and repeat multipliers are applied. Performance carries a weight of 8 in the overall score.

It shares the performance.assets family ceiling of 25 points: however many findings that family produces, together they cannot remove more than that from Performance. One underlying problem showing up in several places is still one problem.

Severity
low
Default confidence
confirmed
Status when triggered
warn
Deduction
6 points
Family cap
performance.assets · 25
Category
Performance
Module
Perf assets
Fix owned by
user
In the ruleset since
2026.09

How the whole score is calculated

How to fix it

Give fingerprinted static assets a one-year immutable cache lifetime.

Returning visitors currently re-download the files listed above even though they have not changed.

  1. Put a content hash in the file name of every build artefact — app.4f3a9c2e.js rather than app.js. Most bundlers do this with one setting.

  2. Serve anything whose name contains a hash with Cache-Control: public, max-age=31536000, immutable.

  3. Leave anything **not** fingerprinted on a short lifetime. A long max-age on a stable URL means you cannot ship a fix to visitors who already have it.

  4. Apply the same policy at the CDN as at the origin — a CDN that overrides the origin's header is the usual reason this finding persists after the server is fixed.

  5. Do not set a long lifetime on the HTML document; it is what tells the browser about the new asset URLs.

How to confirm it worked

  • Re-run this scan and confirm the short-cache count has dropped.

  • curl -sSI https://‹host›/path/to/app.<hash>.js | grep -i cache-control — expect public, max-age=31536000, immutable

A named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.

Remediation by platform

nginx
location ~* "\.[0-9a-f]{8,}\.(?:css|js|woff2|png|jpg|avif|webp|svg)$" {
	add_header Cache-Control "public, max-age=31536000, immutable" always;
}
Apache
<IfModule mod_headers.c>
	<FilesMatch "\.[0-9a-f]{8,}\.(css|js|woff2|png|jpg|avif|webp|svg)$">
		Header always set Cache-Control "public, max-age=31536000, immutable"
	</FilesMatch>
</IfModule>
Caddy
@immutable path_regexp \.[0-9a-f]{8,}\.(css|js|woff2|png|jpg|avif|webp|svg)$
header @immutable Cache-Control "public, max-age=31536000, immutable"
  • The pattern deliberately matches only names containing a hash. Assets without one keep whatever the default is, which is what stops an un-fingerprinted file from becoming unfixable for a year.

  • Place this in the vhost or in the directory config for the asset root, not in a .htaccess at the site root, or it will also match paths you did not intend.

  • Caddy matchers are evaluated in order; put this before any broader header directive that would otherwise win.

Technical detail

Static subresources with an inefficient cache policy: ‹count›.

‹summary›

‹detail›

**How this was measured, and what it could not see.** We fetched the page and then the subresources its HTML declares, and read what the server sent back. No browser was involved, so nothing here depends on how fast our machine is — but equally, **a resource that a script loads after the page arrives is invisible to this check**. A page that assembles its own asset list at runtime will look lighter here than it is. The number of subresources the page declared, and how many of those we asked about, are both stated in the page-weight finding; where they differ, this section examined a prioritised sample rather than the whole page.

**What the figure counts.** The bytes listed are the transfer a returning visitor pays for a second time, which is the whole of each file. A response that declares no-store is **not** reported: that is a deliberate instruction from the server and telling its owner to cache it would be advice against their stated intent.

**This is about the files the page loads, not about the page itself.** The main document's Cache-Control is checked separately by http.headers.no-cache-control and http.headers.cache-directives-conflicting. The document *should* have a short lifetime — it changes. These subresources should not, and that is the difference the two findings are drawing.

The rule that makes a long lifetime safe is **content-addressed URLs**: if the file name contains a hash of its contents, a changed file is a different URL, so nothing can be stale. With that in place Cache-Control: public, max-age=31536000, immutable is correct, and immutable additionally tells the browser not to revalidate on a reload (RFC 8246). Without it, a long lifetime is dangerous — you cannot deploy a fix — so fix the file naming first and the header second.

Standards and references

Test this on your domain

Run the check that produces this finding, on its own, against any domain.

Open the perf assets checker