A Practical Guide to Browser Caching for Web Apps
Source: Dev.to
What browser caching is
- Browser cache – local to the user’s device (fastest path for repeat visits).
- CDN / proxy cache – copies at edge servers closer to users (reduces origin load and latency).
- Service‑worker cache (optional) – app‑controlled caching logic for offline and advanced update strategies.
Why caching matters
- Performance – returning visitors see dramatic speed gains.
- Cost – fewer bytes leave your origin and APIs.
- Reliability – less load on your servers during traffic spikes and incidents.
The main challenge: staying fresh after a deployment
The golden pattern (simple and reliable)
- HTML revalidates on every navigation.
- Static assets (JS, CSS, images) get long cache lifetimes, but their filenames change when their content changes.
- APIs revalidate frequently using ETag or Last‑Modified.
How to implement it
1️⃣ Content‑hashed filenames for static assets
2️⃣ Cache‑Control headers
| Resource | Header example | Reason |
|---|---|---|
| HTML | Cache-Control: no-cache, must-revalidate (or max-age=0, must-revalidate) | Tells the browser and CDN to check with the server before using a cached copy. With ETag or Last‑Modified, the check is cheap and fast. |
| Hashed assets (JS/CSS/images/fonts) | Cache-Control: public, max-age=31536000, immutable | Long‑lived because the URL is unique to the content. |
| APIs | Cache-Control: no-cache (or a short max-age with must-revalidate) + ETag or Last‑Modified | Clients receive quick 304 Not Modified responses when data hasn’t changed. |
3️⃣ Deployment order
- Upload new hashed assets first.
- Publish the updated HTML that references those new asset filenames.
- (Optional) Invalidate CDN cache for HTML routes so the updated entry point propagates quickly.
When to use caching
- Production – always. It’s a foundational performance practice.
- Development – keep caching minimal to avoid confusion (e.g., disable cache in DevTools or use a short
max-age). - Private or sensitive content – use stricter headers such as
Cache-Control: no-storefor confidential pages or data.
What “no‑cache” actually means
It does not mean “don’t store”; it means “must revalidate with the origin before using a cached copy.”
Example user flow after a deploy
-
You deploy a build
main.jschanges → becomesmain.<hash>.jsstyles.cssunchanged → remainsstyles.<hash>.cssindex.htmlis updated to reference the new filenames
-
A user visits
- Browser revalidates
index.htmland gets the updated HTML. - It downloads
main.<hash>.js(new URL). - It reuses cached
styles.<hash>.css(same URL).
- Browser revalidates
Result: only changed files are fetched; unchanged files load instantly.
Configuration examples (conceptual)
Nginx
# HTML – always revalidate
location = /index.html {
add_header Cache-Control "no-cache, must-revalidate";
}
# Static assets – long‑lived immutable cache
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Apache (.htaccess)
# HTML
Header set Cache-Control "no-cache, must-revalidate"
# Static assets
Header set Cache-Control "public, max-age=31536000, immutable"
Node / Express
app.use(
express.static("dist", {
setHeaders: (res, path) => {
if (path.endsWith(".html")) {
res.setHeader("Cache-Control", "no-cache, must-revalidate");
} else {
res.setHeader(
"Cache-Control",
"public, max-age=31536000, immutable"
);
}
},
})
);
Framework tips
- React (Vite/CRA), Angular, Vue CLI, Next.js, Nuxt – production builds usually create content‑hashed filenames automatically. Verify that the
dist/buildoutput contains hashes and serve those assets with the long‑lived immutable header. - SSR frameworks (Next.js, Nuxt) – let the framework manage asset hashing. Ensure HTML responses have revalidation headers or a short CDN TTL with
must-revalidate. Dynamic pages should emitETagorLast‑Modifiedif feasible. - Single‑Page Apps – always revalidate
index.html. For deep links, serveindex.htmlfor app routes with the same headers.
CDN best practices
- Let your origin send the headers above; most CDNs respect them.
- Invalidate/purge HTML routes after deployment so the updated entry point becomes visible quickly.
- With hashed assets you rarely need to purge JS/CSS because new builds use new filenames.
- Consider a short CDN TTL for HTML (e.g., 60–300 seconds) as a safety net if purges are missed.
- Keep older hashed assets on the CDN (and origin) for a while to avoid 404s for users still referencing previous builds and to support rollbacks.
APIs and data freshness
- Use
ETagorLast‑ModifiedwithCache-Control: no-cache(or a shortmax-age+must-revalidate). This avoids stale data and keeps bandwidth low via 304 responses. - For highly dynamic or sensitive responses that must never be reused, use
Cache-Control: no-store.
Service workers (optional, advanced)
-
Service workers let you script caching and offline behavior.
-
Version your caches (e.g.,
app-cache-v42) and precache assets on install for predictable offline behavior. -
On each deploy, publish a new service worker. Decide your update UX:
- Prompt users to refresh when an update is available (good control and clarity).
- Auto‑activate with
self.skipWaiting()andclients.claim()(faster, but consider UX trade‑offs).
-
Never let the service worker serve stale HTML forever. Use a network‑first or stale‑while‑revalidate strategy for HTML so updates are discovered promptly.
How to force revalidation
-
For yourself: hard refresh (
Ctrl/Cmd+Shift+R) or enable “Disable cache” in DevTools. -
For all users:
- Keep HTML as
no-cache, must-revalidateand returnETagorLast‑Modified. - Invalidate CDN cache for HTML routes immediately after deploy.
- In emergencies, temporarily set
Cache-Control: no-storeon HTML to force a refresh, then revert.
- Keep HTML as
How to verify your setup
Browser DevTools → Network
index.htmlshould show 200 or 304 after reload (not “from cache”), indicating revalidation.- Hashed JS/CSS should typically show “from disk cache” or “from memory cache” between deployments.
curl checks
# Get headers (look for ETag)
curl -I https://your.site/index.html
# Conditional request – expect 304 if unchanged
curl -H "If-None-Match: <etag-value>" -I https://your.site/index.html
Common pitfalls
- Skipping content hashing → leads to stale files and complex purges. Always use hashed filenames for cache‑busting.
- Setting overly aggressive
max-ageon HTML → prevents updates from reaching users promptly. - Forgetting to purge CDN HTML after a deploy → users may keep receiving the old entry point.
- Mis‑configuring
immutableon resources that can change without a filename change → browsers will never revalidate them.
Cache‑Busting & Long‑Lived Caching Guidelines
Query‑string cache busting
file.js?v=123– Some caches ignore query parameters.- Prefer content‑hashed filenames (e.g.,
file.1a2b3c.js).
Long‑lived caching for HTML
- HTML must be revalidated on each request; otherwise users won’t see new builds.
Removing old assets immediately
- Users with an older HTML page may still request previously‑hashed files.
- Keep assets from recent builds available for a safe window (e.g., 24 h).
Service‑worker traps
- A service worker that serves stale HTML indefinitely breaks updates.
- Ensure HTML is revalidated and that you have a clear update strategy (version bump, prompt, auto‑activation, etc.).
Security & Privacy Notes
- Never cache sensitive or private data. Use
Cache-Control: no-storefor such responses. - When using third‑party CDNs, plugins, or service‑worker libraries, verify they meet your organization’s security and compliance requirements.
- At Oracle: confirm alignment with internal guidelines before adopting external tools.
Simple Deployment Checklist
-
Build
- Output content‑hashed filenames for all static assets.
-
HTML responses
Cache-Control: no-cache, must-revalidate- Include
ETagorLast‑Modified.
-
Static assets
Cache-Control: public, max-age=31536000, immutable
-
Publish order
- Upload new hashed assets first.
- Then publish the updated HTML.
-
CDN
- Invalidate CDN cache for HTML routes after deployment (recommended).
-
Rollback safety
- Keep the last few builds’ assets available for rollbacks and late‑returning users.
-
Service worker (if used)
- Bump the worker version.
- Implement an update prompt or auto‑activation strategy.
FAQ
| Question | Answer |
|---|---|
| Will users always get the latest build? | Yes. HTML revalidates and points to new hashed assets; changed assets download, unchanged assets are reused from cache. |
| What if only JS changed? | The HTML updates the <script> tag to the new hash; revalidation picks it up automatically. |
| Do I need users to hard‑refresh? | No – hashing plus proper headers handles updates transparently. |
| Is “no‑cache” slow? | No. With ETag or Last‑Modified, the browser typically receives a quick 304 Not Modified and reuses its local copy. |
| Can I skip CDN purges? | Usually yes for assets (they’re hashed). Purge HTML for immediate propagation. |
Closing Thoughts
This pattern—revalidated HTML, hashed assets with long‑lived immutable caching, and APIs with validators—delivers fast loads and safe updates with minimal operational overhead.
- Start with the basics above.
- Verify behavior in the browser’s Network panel.
- Iterate as your app grows.
If you plan to use third‑party tools or CDNs, confirm they comply with your organization’s security and privacy standards.
At Oracle: please verify alignment with internal guidelines before adopting external tools.