Measured in your browserWe advise on speed. We practice it.Loaded just now · real numbers from this visit, not a lab score.
Page loaded
First byte
DOM ready
First paint
Largest paint
DNS lookup
TLS handshake
Transferred
Saved by compression
Requests

Versioned assets are the one place caching can be perfect: content that never changes at its URL can cache forever, everywhere, with zero revalidation. Getting there permanently takes four decisions — hash scheme, reference discipline, deploy order, retention — and this guide makes all four, including the anti-patterns that still bite teams that half-adopt it.

Why versioning beats every alternative

All cache-freshness strategies for static assets are workarounds for one problem: the URL stays the same while the content changes. Versioning dissolves the problem instead of managing it — every content change ships at a new URL, so the URL-to-content mapping becomes permanent and every cache in the world (edge, browser, corporate proxy you've never heard of) can hold it forever without ever being wrong. That single property is why versioned assets get the max-age=31536000, immutable treatment from the Cache-Control guide and why no purge, per the purge guide, is ever needed for them: you don't invalidate old versions, you simply stop referencing them. Everything else in asset delivery is easier downstream of this decision.

Filename hashes vs query strings

Two mechanical schemes exist. Content-hashed filenames (app.3f9c2b.js), emitted by every modern bundler, are the gold standard: the version is derived from the bytes (identical content = identical URL, changed content = changed URL, automatically), and no cache anywhere mishandles a plain path. Query-string versions (app.js?v=57) work nearly as well on modern CDNs — but require the query string in the cache key for those paths (the include-all mode from the cache-key guide), historically confused some intermediary caches, and, when the version is a manual number or deploy timestamp rather than a content hash, invalidate everything on every deploy whether it changed or not. Use filename hashing wherever a build pipeline exists; accept query versions for platforms (older CMS themes, plugin ecosystems) where they're what the tooling emits — WordPress's ?ver= convention is exactly this scheme, and it works. What you never do is neither.

The reference chain rule

Versioning works when the chain of references is airtight: something un-versioned (the HTML page, per the SPA pattern's fresh index) points to versioned assets, which point only to other versioned assets. The rule: every reference to a versioned asset goes through the build's manifest — templates ask the manifest for "the current app.js", never hardcode a hashed name (they go stale) or an un-hashed one (it escapes versioning). The subtle violations live inside assets: a CSS file referencing background.png by plain name, a JS chunk importing by un-hashed path — modern bundlers rewrite these too, but only for assets that flow through them, so the audit is grep-shaped: no reference to your asset domains anywhere in shipped output should lack a hash, except the entry HTML itself. One un-versioned link in the chain reintroduces the entire freshness problem for everything behind it.

Deploy order and retention

Two operational rules make versioning safe under deployment. Order: new assets upload and verify first, the referencing HTML publishes last — publishing references before their targets exist is the one way versioned estates serve 404s. Retention: old versions stay available after a deploy rather than being deleted at cutover, because cached HTML and long-lived browser tabs keep referencing them — a day covers page caches, a week covers open tabs, and storage this small is effectively free (especially on the zero-egress origins from the bucket comparison). Teams that skip retention meet it as the chunk-load-error bug that only affects users who had the site open during a deploy — intermittent, unreproducible, and permanently fixed by keeping two weeks of old hashes around.

Anti-patterns that still bite

Four survivors from the folklore era still cause incidents. Deploy-stamp versioning (?v=BUILD_NUMBER on every asset) — busts every cache on every deploy, converting your release cadence into your miss rate; hash content, not deployments. Partial adoption — hashed JS but plain-named images, versioned app but un-versioned vendor bundle; the un-versioned remainder gets the short TTLs and the staleness bugs, and the team concludes versioning "doesn't fully work". Hash-then-purge — running purges on hashed paths at deploy "to be safe", which spends purge quota to invalidate content that cannot be stale and stampedes misses on assets that were perfectly cached. And versioning the HTML itself — hashed page URLs break bookmarks, links and SEO; HTML is the fresh entry point by design, and its freshness comes from short TTLs and event purging, not from versioning. Versioning is for the leaves; the root stays live.

Get the free assessmentMore analysis