A Practical Guide to Browser Caching for Web Apps

Published: (January 6, 2026 at 09:59 PM EST)
6 min read
Source: Dev.to

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)

  1. HTML revalidates on every navigation.
  2. Static assets (JS, CSS, images) get long cache lifetimes, but their filenames change when their content changes.
  3. APIs revalidate frequently using ETag or Last‑Modified.

How to implement it

1️⃣ Content‑hashed filenames for static assets

2️⃣ Cache‑Control headers

ResourceHeader exampleReason
HTMLCache-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, immutableLong‑lived because the URL is unique to the content.
APIsCache-Control: no-cache (or a short max-age with must-revalidate) + ETag or Last‑ModifiedClients receive quick 304 Not Modified responses when data hasn’t changed.

3️⃣ Deployment order

  1. Upload new hashed assets first.
  2. Publish the updated HTML that references those new asset filenames.
  3. (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-store for 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

  1. You deploy a build

    • main.js changes → becomes main.<hash>.js
    • styles.css unchanged → remains styles.<hash>.css
    • index.html is updated to reference the new filenames
  2. A user visits

    • Browser revalidates index.html and gets the updated HTML.
    • It downloads main.<hash>.js (new URL).
    • It reuses cached styles.<hash>.css (same URL).

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/build output 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 emit ETag or Last‑Modified if feasible.
  • Single‑Page Apps – always revalidate index.html. For deep links, serve index.html for app routes with the same headers.

CDN best practices

  1. Let your origin send the headers above; most CDNs respect them.
  2. Invalidate/purge HTML routes after deployment so the updated entry point becomes visible quickly.
  3. With hashed assets you rarely need to purge JS/CSS because new builds use new filenames.
  4. Consider a short CDN TTL for HTML (e.g., 60–300 seconds) as a safety net if purges are missed.
  5. 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 ETag or Last‑Modified with Cache-Control: no-cache (or a short max-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:

    1. Prompt users to refresh when an update is available (good control and clarity).
    2. Auto‑activate with self.skipWaiting() and clients.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:

    1. Keep HTML as no-cache, must-revalidate and return ETag or Last‑Modified.
    2. Invalidate CDN cache for HTML routes immediately after deploy.
    3. In emergencies, temporarily set Cache-Control: no-store on HTML to force a refresh, then revert.

How to verify your setup

Browser DevTools → Network

  • index.html should 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-age on 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 immutable on 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-store for 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

  1. Build

    • Output content‑hashed filenames for all static assets.
  2. HTML responses

    • Cache-Control: no-cache, must-revalidate
    • Include ETag or Last‑Modified.
  3. Static assets

    • Cache-Control: public, max-age=31536000, immutable
  4. Publish order

    • Upload new hashed assets first.
    • Then publish the updated HTML.
  5. CDN

    • Invalidate CDN cache for HTML routes after deployment (recommended).
  6. Rollback safety

    • Keep the last few builds’ assets available for rollbacks and late‑returning users.
  7. Service worker (if used)

    • Bump the worker version.
    • Implement an update prompt or auto‑activation strategy.

FAQ

QuestionAnswer
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.

  1. Start with the basics above.
  2. Verify behavior in the browser’s Network panel.
  3. 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.

Back to Blog

Related posts

Read more »