Skip to content
Issue docs

Static files cached for too short a time

Standardshort_static_asset_cacheIssue 183

What is this issue?

When a server sends an image, script, stylesheet or font, its Cache-Control header says how long a browser may keep it. Files with a short lifetime -- minutes, hours, or none at all -- are downloaded again on the next visit even though they have not changed.

This check reports pages whose static files are cached so briefly that a repeat visit would re-download 28 KiB or more.

Why it matters

  • Repeat visits are most visits. A returning visitor whose browser kept last week's files loads the page almost instantly; one who has to fetch them all again does not.
  • It costs your server and CDN bandwidth for files that have not changed.
  • It is usually a one-line server or CDN setting.

Fixing it removes a standard-severity finding from the site's health score.

How to fix it

  1. Give fingerprinted files a year. Files whose name changes when their content does (app.3f9c2.js) can be cached for good:

    Cache-Control: public, max-age=31536000, immutable
  2. Fingerprint the files that are not. Your bundler can add a content hash to every asset name; then step 1 applies to all of them.

  3. Keep the HTML short-lived. The page itself should revalidate (no-cache or a short max-age), so a new release is picked up immediately and points at new file names.

  4. Set it at the CDN if you cannot change the origin's headers.

Examples

1. No cache header at all

Fails -- 96 KiB re-downloaded per visit:

GET /js/app.js
Cache-Control: (none)

Passes:

GET /js/app.3f9c2.js
Cache-Control: public, max-age=31536000, immutable

2. A ten-minute lifetime on images

Fails -- the hero image is cached for max-age=600, so a visitor returning the next day downloads it again.

Passes -- max-age=2592000 (30 days) on images whose URLs never change content.

How PixyScan detects this

  1. Chooses which pages to measure. After the crawl, PixyScan measures the homepage plus the pages your own site links to most, up to a per-scan budget (ten pages by default, at most fifty). Measuring every page would take hours and exhaust the PageSpeed Insights quota of the API key it runs under.

  2. Asks the PageSpeed Insights API about each of them, on mobile. This needs the Hobby plan or above and a PageSpeed Insights API key on the site or its workspace.

  3. Reads the Lighthouse audit uses-long-cache-ttl (in newer Lighthouse versions, cache-insight), which lists each static file with a short cache lifetime and estimates the bytes a repeat visit would download again.

  4. Raises the issue when those bytes reach 28 KiB -- the point at which Lighthouse's own score for the audit falls below 0.9 and PageSpeed stops drawing it green.

    Lighthouse renamed several audits in versions 12 and 13 (the "insight" audits shared with Chrome DevTools). PixyScan reads whichever one the response carries, and the finding names the audit it was read from.

  5. Raises nothing on a page it did not measure. A page outside the sample, and a page whose measurement failed, are both reported as Not measured. Neither is treated as passing, and neither can fail this check.

What we store

Storage Level

Page Level


Database Table / Prisma Model

PagePerformance (page_performance), with the finding itself on audit_issues.details.


Fields Used

Field Type Description
strategy String mobile or desktop. Every number in the row belongs to one form factor.
fetchedAt DateTime When PageSpeed answered. "Measured three weeks ago" is not "not measured".
errorReason String? Why a sampled page still has no numbers. Null when the call succeeded.
audits Json? A compact summary of the Lighthouse audits the opportunity checks read: per audit its source (the Lighthouse audit id it was read from), score, numericValue, displayValue, itemCount, totals over every row (wastedBytes, wastedMs, blockingTime, mainThreadTime) and the five worst rows. Keyed by the classic Lighthouse audit id. Null when the response carried no audits.

On the finding (audit_issues.details): message, plus audits (uses-long-cache-ttl or cache-insight), wastedBytes, thresholdBytes (28672), fileCount and items (the five files with the most bytes at stake, each url, cacheLifetimeMs, totalBytes, wastedBytes).


Detection Dependencies

  • PageSpeed Insights API v5 (lighthouseResult.audits), mobile strategy
  • The PageSpeed plan feature (Hobby and above) and a PageSpeed Insights API key
  • The crawl's internal link graph, which decides which pages are sampled

Note

A page with no row was never measured. The pass samples the homepage plus the most-linked pages within a per-scan budget (default 10, at most 50), so most pages of a site have no page_performance row at all. That is the normal state, it is drawn as Not measured, and this check cannot be raised against it.

A row whose metric columns are all null with an errorReason set is a different state again: the page WAS sampled and the measurement failed. It is still never reported as passing.

Further reading