← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Asset Versioning & Browser Cache Strategy

Performance Beginner
debt(d3/e3/b3/t5)
d3 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'default linter catches the common case' (d3) — Lighthouse audits flag missing Cache-Control headers and inefficient cache policies on static assets directly, and curl can verify headers easily.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3) — quick_fix is configuring build tool (webpack/vite) to emit hashed filenames and adjusting server cache headers; touches build config and server config but is a well-known pattern.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3) — applies only to web context's static asset pipeline; once configured the build tool handles it, but it does shape the deployment/asset-serving layer.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5) — the misconception (long max-age = stale users) is exactly the documented gotcha most devs learn; query-string busting and missing immutable directive are subtle pitfalls that contradict naive intuition.

About DEBT scoring →

Also Known As

browser caching static asset cache cache busting

TL;DR

Serving static assets with immutable long-lived cache headers plus content-hash filenames — maximising cache hits while guaranteeing instant cache-busting on change.

Explanation

The optimal static asset strategy: append a content hash to filenames (app.a3f8d2.js) and serve with Cache-Control: public, max-age=31536000, immutable. Browsers cache indefinitely; on next deploy the hash changes so the new filename is fetched fresh — no stale asset problem. The HTML document itself gets Cache-Control: no-cache, must-revalidate so it always reflects the latest asset filenames. Build tools (Vite, Webpack, Laravel Mix) handle hash generation and manifest files that PHP uses to resolve hashed filenames. For PHP-served dynamic content: use ETag or Last-Modified headers and respond with 304 Not Modified to save bandwidth. Avoid query-string versioning (?v=123) — some CDNs and proxies strip or ignore query strings when caching.

Diagram

flowchart LR
    subgraph No Content Hash
        BROWSER[Browser] -->|GET app.css| SERVER[Server]
        SERVER -->|200 app.css v1| BROWSER
        DEPLOY[New deploy v2] --> SERVER
        BROWSER -->|cached - serves v1!| OLD[Stale CSS]
    end
    subgraph Content Hash in Filename
        B2[Browser] -->|GET app.a3f2c.css| S2[Server]
        S2 -->|200 Cache-Control: max-age=31536000| B2
        DEPLOY2[New deploy] --> NEW[app.b7d9e.css - new hash]
        B2 -->|new filename - fetches fresh| FRESH[Always fresh]
    end
    style OLD fill:#f85149,color:#fff
    style FRESH fill:#238636,color:#fff

Common Misconception

Setting a long Cache-Control max-age means users always get stale assets after a deployment. Cache busting via content-hashed filenames (app.a1b2c3.js) lets you set immutable long-lived cache headers while guaranteeing users receive updated files immediately after deployment.

Why It Matters

Long cache lifetimes for static assets eliminate repeated downloads — cache-busting via content hashes ensures users get updated files immediately without waiting for cache expiry.

Common Mistakes

  • Using a query string for cache-busting (?v=2) — some proxies ignore query strings and serve stale content.
  • Short Cache-Control max-age on static assets — defeats the performance benefit.
  • Not using immutable directive for fingerprinted assets — browsers may still revalidate without it.
  • Cache-Control: no-cache on static assets in production — every request hits the server.

Code Examples

✗ Vulnerable
// Version query string — proxy-unfriendly cache busting:
<script src="/app.js?v=1.2"></script>

// Content-hash filename — correct approach:
<script src="/app.abc123f.js"></script>
<!-- Server: Cache-Control: public, max-age=31536000, immutable -->
✓ Fixed
// Content-hash filenames + immutable cache headers
// Build tool (Vite) output: app.a3f8d2b1.js

// PHP: read Vite manifest to resolve hashed filenames
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
$assetUrl = '/build/' . $manifest['resources/js/app.js']['file'];
// → /build/assets/app-a3f8d2b1.js

// Serve with immutable header (nginx)
location ~* \.(js|css|woff2|png|webp)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Vary Accept-Encoding;
}

// HTML document: no-cache so browsers always check for new asset filenames
add_header Cache-Control "no-cache, must-revalidate";

Added 15 Mar 2026
Edited 22 Mar 2026
Views 73
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 0 pings S 3 pings S 2 pings M 1 ping T 0 pings W 0 pings T 2 pings F 0 pings S 0 pings S 0 pings M 3 pings T 1 ping W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 1 ping T 0 pings W
No pings yet today
PetalBot 1
Google 8 Perplexity 8 Amazonbot 7 Scrapy 7 SEMrush 5 Ahrefs 4 ChatGPT 4 Unknown AI 2 Claude 2 Bing 2 Meta AI 1 PetalBot 1
crawler 45 crawler_json 6
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Low
⚡ Quick Fix
Serve hashed filenames (app.abc123.js) with Cache-Control: immutable, max-age=31536000 — the hash changes when content changes, so the cache is always correct
📦 Applies To
any web
🔗 Prerequisites
🔍 Detection Hints
Static assets without content hash in filename; Cache-Control missing on CSS/JS; assets with very short max-age requiring frequent revalidation
Auto-detectable: ✓ Yes lighthouse curl webpack vite
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Low ✓ Auto-fixable Fix: Low Context: File


✓ schema.org compliant