Skip to content

frontend-performance/cache-header-lint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cache-header-lint

Crawl a deployed page, discover its static assets, fetch their HTTP response headers, and audit the caching configuration — flagging the misconfigurations that quietly waste bandwidth or serve stale content. Each finding links to a deep-dive explainer on frontend-performance.com.

No headless browser, no Lighthouse. Just built-in fetch + a tiny HTML parse, so it runs in a second and drops into any CI step or PR check.

cache-header-lint  https://example.com/
HTTP 200  ·  14 assets

/assets/app.4f3a2b1c.js  [script, hashed]
  cache-control: public, max-age=600
  ! hashed-short-max-age  Content-hashed asset has max-age=600 (10m); a fingerprinted file should use max-age=31536000.
      Recommended: Cache-Control: public, max-age=31536000, immutable.
      → https://frontend-performance.com/advanced-caching-strategies-cdn-architecture/http-cache-control-headers-explained/

Why

A content-hashed bundle like app.4f3a2b1c.js can be cached forever — its URL changes whenever its bytes change. If it ships with max-age=600 (or, worse, no-store), every visitor re-downloads it constantly. Conversely, an HTML document marked immutable pins visitors to a stale page until the URL changes. These mistakes are invisible in a browser and easy to miss in review. cache-header-lint makes them a one-line check.

Install

Not published to npm. Run it straight from GitHub with npx:

npx github:frontend-performance/cache-header-lint https://example.com

Or clone and build from source:

git clone https://github.com/frontend-performance/cache-header-lint.git
cd cache-header-lint
npm install        # builds via the prepare script
npm link           # exposes the `cache-header-lint` binary

Requires Node.js 20+ (uses the built-in global fetch).

Usage

cache-header-lint <url> [options]
cache-header-lint https://example.com
cache-header-lint https://example.com --include-cross-origin
cache-header-lint https://example.com --json > report.json
cache-header-lint https://preview.example.com --markdown >> "$GITHUB_STEP_SUMMARY"
cache-header-lint https://example.com --fail-on warn   # stricter CI gate

Options

Option Default Description
--include-cross-origin off Also audit assets served from other origins.
--concurrency <n> 8 Parallel header fetches.
--timeout <ms> 15000 Per-request timeout.
--max-assets <n> 100 Cap the number of assets audited.
--delay <ms> 0 Polite delay between fetches, per worker.
--fail-on <level> error Exit-1 threshold: error | warn | never.
--json Machine-readable JSON.
--markdown Markdown table (for CI job summaries).
--no-color Disable ANSI colors (also respects NO_COLOR).
-h, --help Show help.

Exit codes

  • 0 — no findings at or above --fail-on
  • 1 — at least one finding at or above --fail-on (default: error)
  • 2 — bad arguments, or the target page itself could not be fetched

What it checks

Findings have a severity (error / warn / info) and a "learn more" link.

Rule Severity Applies to Meaning
missing-cache-control error static asset No Cache-Control (or Expires) at all — caching is left to browser guesswork.
hashed-short-max-age warn hashed asset Content-hashed filename but max-age is missing or well under a year.
hashed-no-store warn hashed asset Hashed asset sent with no-store/no-cache — almost always a misconfig.
hashed-missing-immutable info hashed asset Long max-age but no immutable, so browsers still revalidate on reload.
non-hashed-long-max-age warn static asset Non-fingerprinted asset cached ~1 year with no way to bust it → stale risk.
missing-validators info static asset Cacheable but no ETag/Last-Modified, so revalidation re-downloads the body.
html-immutable warn document The HTML document is marked immutable — visitors get pinned to stale markup.
html-long-max-age warn document The document has a long max-age with no revalidation — deploys don't reach cached visitors.
html-no-cache-control info document The document has no Cache-Control; heuristic caching can serve stale markup.
cdn-cache-status info any Reports cf-cache-status / x-cache / x-vercel-cache (and notes repeated MISS/BYPASS).
no-cdn-detected info run No CDN/edge cache headers seen on any asset.

→ Background reading: HTTP Cache-Control headers explained, Cache invalidation patterns, CDN edge caching configuration, stale-while-revalidate implementation.

How it works

  1. Fetch the page HTML with fetch (following redirects).
  2. Extract asset references from the markup with cheerio: <script src>, <link rel="stylesheet">, <link rel="preload"/"modulepreload">, <img src/srcset>, <source srcset>, and fonts. Relative URLs resolve against the page (honouring <base href>); cross-origin assets are skipped unless --include-cross-origin is set.
  3. Fetch each asset's headers with a concurrency-limited pool (default 8). It issues a GET with a 1-byte Range and cancels the body immediately, so headers are read without downloading the payload (with a HEAD fallback for servers that reject the method). Individual failures are reported, never fatal.
  4. Audit the headers with a set of pure functions (src/rules.ts). The filename hash heuristic (src/hash.ts) decides whether an asset is fingerprinted — hex runs of 8+ chars, or mixed base-62 tokens containing a digit (e.g. app.4f3a2b1c.js, chunk-AbC123.css, style.DWA7XW_Z.css) — which determines how strict the caching expectations are. The document itself is audited with the same engine but a document-appropriate rule set.

The audit core is deliberately pure (URL + headers → findings) and is the most heavily unit-tested part of the codebase.

Programmatic API

import { lint } from "cache-header-lint";

const result = await lint("https://example.com", {
  includeCrossOrigin: false,
  concurrency: 8,
  timeout: 15_000,
  maxAssets: 100,
});

console.log(result.summary); // { assets, fetched, failures, error, warn, info }
for (const asset of result.assets) {
  for (const finding of asset.findings) {
    console.log(asset.url, finding.severity, finding.rule, finding.message);
  }
}

The pure primitives are exported too: auditAsset({ url, kind, headers }), parseCacheControl(value), isHashedFilename(url), and extractAssets(html, pageUrl).

GitHub Action

A composite action is included. It runs the linter against a deploy-preview URL and appends a Markdown report to the job summary.

# .github/workflows/cache-audit.yml
name: Cache header audit
on:
  deployment_status:

jobs:
  audit:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: frontend-performance/cache-header-lint@main
        with:
          url: ${{ github.event.deployment_status.target_url }}
          fail-on: warn

Or call the CLI directly in any workflow step:

      - run: |
          npx --yes github:frontend-performance/cache-header-lint "$PREVIEW_URL" \
            --markdown >> "$GITHUB_STEP_SUMMARY"
          npx --yes github:frontend-performance/cache-header-lint "$PREVIEW_URL" \
            --fail-on error

Action inputs

Input Default Description
url (required) The deployed / preview URL to audit.
fail-on error Exit-1 threshold: error | warn | never.
include-cross-origin false Also audit cross-origin assets.
concurrency 8 Parallel header fetches.
max-assets 100 Maximum number of assets to audit.
timeout 15000 Per-request timeout in ms.

Further reading

Deep dives on the concepts this tool checks, from frontend-performance.com:

License

MIT © frontend-performance. See LICENSE.

About

CLI + GitHub Action that crawls a deployed page and audits caching headers (immutable hashed assets, Cache-Control, ETags, CDN hints), flagging misconfigurations. Explainers on frontend-performance.com.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors