Static files are re-downloaded because they are barely cached
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 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.
Put a content hash in the file name of every build artefact —
app.4f3a9c2e.jsrather thanapp.js. Most bundlers do this with one setting.Serve anything whose name contains a hash with
Cache-Control: public, max-age=31536000, immutable.Leave anything **not** fingerprinted on a short lifetime. A long
max-ageon a stable URL means you cannot ship a fix to visitors who already have it.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.
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
location ~* "\.[0-9a-f]{8,}\.(?:css|js|woff2|png|jpg|avif|webp|svg)$" {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}<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>@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
.htaccessat the site root, or it will also match paths you did not intend.Caddy matchers are evaluated in order; put this before any broader
headerdirective 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.