dotvitals

Static files are re-downloaded because they are barely cached

LowHigh confidenceQuick winperf.lab.static-assets-short-cache

What this check looks for

‹count› static file(s) this page loads — scripts, stylesheets, fonts, images — are served with a short or missing cache lifetime, so returning visitors fetch them again. An estimated ‹savings bytes› per repeat visit.

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
high
Status when triggered
warn
Deduction
6 points
Family cap
performance.assets · 25
Category
Performance
Module
Perf lab
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 an estimated ‹savings bytes› of files that 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 uncacheable-fixable 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›, estimated saving **‹savings bytes›** per repeat visit.

‹detail›

This came from **one page load we performed**, not from your visitors. A lab measurement is reproducible and tells you exactly which bytes and which resources are responsible — which is why the numbers below are byte counts and resource lists rather than a score. What real visitors experience is in the field-data section, which is the evidence this report scores Core Web Vitals on.

**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, which fetch that response directly. 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 lab checker

Other perf lab checks