Skip to main content

Browser Caching - Complete Guide

Table of Contentsโ€‹

  1. Why Browser Caching Matters
  2. Types of Browser Caches
  3. HTTP Cache-Control
  4. ETag vs Last-Modified
  5. Versioned Asset Caching
  6. HTML vs Static Assets Caching
  7. Service Worker Caching Strategies
  8. CDN Caching
  9. API Response Caching
  10. Common Interview Questions
  11. Recommended Production Setup
  12. Quick Reference Summary

Why Browser Caching Mattersโ€‹

Browser caching is critical for web performance optimization. It provides:

  • Reduced network requests - Serves resources from local storage instead of downloading
  • Improved page load performance - Faster LCP (Largest Contentful Paint) and TTI (Time to Interactive)
  • Lower server load - Fewer requests to origin servers
  • Enhanced offline experiences - Access to cached content without network connectivity

Types of Browser Cachesโ€‹

Memory Cacheโ€‹

Memory cache stores resources in RAM for ultra-fast access.

Characteristics:

  • Fastest cache available
  • Volatile - cleared on page refresh or tab close
  • Limited capacity - constrained by available memory

Typical Use Cases:

  • JavaScript modules during page session
  • Recently fetched resources
  • Inline scripts and styles

Example:

// Module stays in memory cache during session
import utils from './utils.js';
import helpers from './helpers.js';

DevTools Indicator: Network tab โ†’ Size column: (from memory cache)


Disk Cacheโ€‹

Disk cache persists resources on the file system across browser sessions.

Characteristics:

  • Persistent - survives browser restarts
  • Larger capacity - more storage than memory cache
  • Slower than memory cache but faster than network

Typical Use Cases:

  • Images (PNG, JPG, SVG, WebP)
  • CSS stylesheets
  • JavaScript bundles
  • Web fonts (WOFF, WOFF2)

DevTools Indicator: Network tab โ†’ Size column: (from disk cache)


Service Worker Cacheโ€‹

Service Worker Cache (Cache Storage API) provides programmable, explicit control over caching.

Characteristics:

  • Full programmatic control over cache lifecycle
  • Works offline - enables Progressive Web App (PWA) functionality
  • Explicit management - you decide what, when, and how to cache

Example:

// Basic service worker cache
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
]);
})
);
});

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});

HTTP Cache-Controlโ€‹

The Cache-Control header is the most important mechanism for browser caching. It defines how and for how long resources should be cached.

Strong Cachingโ€‹

Strong caching means the browser serves from cache without making any network request.

Header:

Cache-Control: max-age=31536000, immutable

Directives:

  • max-age=31536000 - Cache for 1 year (in seconds)
  • immutable - Tells browser content will never change

Best For:

  • Versioned static assets with content hashes
  • Examples: app.8d9f7c2a.js, styles.4b3e9f.css

Behavior:

  1. Browser checks cache first
  2. If valid, serves immediately
  3. No network request made
  4. Maximum performance

Revalidation Cachingโ€‹

Revalidation caching makes a conditional request to check if cached content is still valid.

Header:

Cache-Control: no-cache

Important: Despite the name, no-cache does not mean "don't cache". It means "cache but revalidate before using".

Request Validators:

If-None-Match: "etag-abc123"
If-Modified-Since: Wed, 20 Sep 2023 10:30:00 GMT

Server Responses:

HTTP/1.1 304 Not Modified
(No body sent - browser uses cached version)

Or if modified:

HTTP/1.1 200 OK
(Full response body with updated content)

Best For:

  • HTML files
  • API endpoints that need freshness checks
  • Resources that change occasionally

No Cachingโ€‹

Prevents all caching of the resource.

Header:

Cache-Control: no-store

Behavior:

  • Browser never caches the response
  • Every request goes to the server
  • No validation, no storage

Use Only For:

  • Sensitive data (authentication tokens, banking info)
  • Personal information
  • CSRF tokens

ETag vs Last-Modifiedโ€‹

Both headers enable conditional requests for cache revalidation, but they work differently.

HeaderDescriptionHow It WorksLimitations
ETagHash or version of resourceServer generates hash of content; browser sends If-None-MatchMulti-node servers may generate different ETags
Last-ModifiedTimestamp of last changeServer sends modification time; browser sends If-Modified-SinceSecond-level precision only (not millisecond)

Best Practice:

Cache-Control: no-cache
ETag: "v123-abc456def"
Last-Modified: Thu, 15 Dec 2024 08:30:00 GMT

Use both ETag and Last-Modified for maximum compatibility and reliability.

Example Flow:

1. Initial Request:
GET /api/data

2. Server Response:
200 OK
ETag: "abc123"
Cache-Control: no-cache

3. Subsequent Request:
GET /api/data
If-None-Match: "abc123"

4. Server Response (if unchanged):
304 Not Modified

Versioned Asset Cachingโ€‹

Versioned asset caching is the industry standard for handling static assets in production.

Strategy: Include a content hash in the filename that changes when content changes.

Examples:

main.9c0a1f2b.js
styles.2ab8cd3e.css
logo.a7f3e9d1.png

Cache Header:

Cache-Control: max-age=31536000, immutable

How It Works:

  1. Build tools (Webpack, Vite, etc.) generate hashed filenames
  2. HTML references these hashed files
  3. Browser caches them aggressively (1 year)
  4. When code changes โ†’ hash changes โ†’ new filename โ†’ automatic cache invalidation

Example Build Output:

<!-- Old deployment -->
<script src="/static/js/main.9c0a1f2b.js"></script>
<link href="/static/css/main.2ab8cd3e.css" rel="stylesheet">

<!-- New deployment (content changed) -->
<script src="/static/js/main.f8d3e7a1.js"></script>
<link href="/static/css/main.c9f2e4b7.css" rel="stylesheet">

Used By:

  • React (Create React App)
  • Next.js
  • Vite
  • Webpack
  • Parcel
  • Rollup

HTML vs Static Assets Cachingโ€‹

Different resource types require different caching strategies.

Resource TypeStrategyHeaderReason
HTMLRevalidateCache-Control: no-cacheHTML references other assets; stale HTML breaks deployments
JS/CSSStrong cacheCache-Control: max-age=31536000, immutableVersioned filenames enable safe long-term caching
ImagesModerate cacheCache-Control: max-age=2592000 (30 days)Less frequently updated; balance between freshness and performance
APIRevalidate or no-storeCache-Control: no-cache or no-storeData changes frequently; freshness critical

Example Configuration:

# nginx.conf example

# HTML files - always revalidate
location ~* \.html$ {
add_header Cache-Control "no-cache";
}

# JS/CSS with hashes - cache forever
location ~* \.[0-9a-f]{8,}\.(js|css)$ {
add_header Cache-Control "max-age=31536000, immutable";
}

# Images - cache for 30 days
location ~* \.(jpg|jpeg|png|gif|svg|webp)$ {
add_header Cache-Control "max-age=2592000";
}

# API - no caching
location /api/ {
add_header Cache-Control "no-store";
}

Service Worker Caching Strategiesโ€‹

Service workers provide powerful, flexible caching patterns. Here are the three most important strategies.

Cache Firstโ€‹

Serve from cache if available, fetch from network as fallback.

Best For: Static assets that rarely change (fonts, icons, framework libraries)

Implementation:

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached version if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then(networkResponse => {
// Optionally cache the network response
return caches.open('v1').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});

Pros:

  • Fastest response time
  • Works completely offline

Cons:

  • May serve stale content
  • Requires explicit cache invalidation

Network Firstโ€‹

Try network first, fall back to cache if offline.

Best For: Dynamic content, API responses, frequently updated data

Implementation:

self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
// Update cache with fresh response
return caches.open('v1').then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// If network fails, try cache
return caches.match(event.request);
})
);
});

Pros:

  • Always tries to get fresh content
  • Graceful offline fallback

Cons:

  • Slower when online (always waits for network)
  • May show stale content when offline

Stale-While-Revalidateโ€‹

Serve cached content immediately while fetching fresh content in the background.

Best For: Best user experience - fast response + fresh content on next load

Implementation:

self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return cache.match(event.request).then(cachedResponse => {
// Fetch fresh version in background
const fetchPromise = fetch(event.request).then(networkResponse => {
// Update cache with fresh response
cache.put(event.request, networkResponse.clone());
return networkResponse;
});

// Return cached version immediately, or wait for network
return cachedResponse || fetchPromise;
});
})
);
});

Pros:

  • Instant response (from cache)
  • Auto-updates cache in background
  • Best perceived performance

Cons:

  • User might see stale content initially
  • Uses bandwidth even when serving from cache

CDN Cachingโ€‹

CDN (Content Delivery Network) caching adds an edge cache layer between users and your origin server.

Key Header:

Cache-Control: public, max-age=600, s-maxage=3600

Directive Meanings:

DirectiveMeaningDuration
publicResponse can be cached by CDN (not just browser)-
max-age=600Browser cache time10 minutes
s-maxage=3600Shared/CDN cache time1 hour

Why Different Durations?

  • Browser cache shorter โ†’ fresher content for repeat visitors
  • CDN cache longer โ†’ reduced origin server load

Example Setup:

// Express.js example
app.get('/api/public-data', (req, res) => {
res.set({
'Cache-Control': 'public, max-age=300, s-maxage=3600',
'ETag': generateETag(data)
});
res.json(data);
});

CDN Behavior:

  1. First user request โ†’ CDN fetches from origin, caches for 1 hour
  2. Next user request โ†’ CDN serves from edge cache (very fast)
  3. After 1 hour โ†’ CDN revalidates with origin
  4. Browser still caches for 5 minutes independently

API Response Cachingโ€‹

Frontend-side API response caching using the Fetch API.

Fetch Cache Options:

fetch('/api/data', {
cache: 'force-cache' // Use cache if available, don't revalidate
});

Available Cache Modes:

ModeBehavior
defaultBrowser default behavior (respects Cache-Control)
no-storeNever cache, always fetch fresh
reloadAlways fetch from network, update cache
no-cacheValidate before using cache (conditional request)
force-cacheUse cache even if stale (offline-first)
only-if-cachedUse cache only, fail if not cached

Practical Examples:

// User profile - cache aggressively
fetch('/api/user/profile', {
cache: 'force-cache'
});

// Live sports scores - always fresh
fetch('/api/scores', {
cache: 'no-store'
});

// News feed - validate freshness
fetch('/api/news', {
cache: 'no-cache'
});

Important: These cache modes work with HTTP cache headers. If server sends Cache-Control: no-store, the browser won't cache regardless of fetch options.


Common Interview Questionsโ€‹

โ“ Why not cache HTML aggressively?โ€‹

Answer: HTML files reference other assets (JS, CSS). If HTML is cached aggressively and you deploy new code:

  1. User gets old HTML from cache (references old app.v1.js)
  2. But app.v1.js might be deleted from server
  3. Result: broken website

Solution: Always revalidate HTML (Cache-Control: no-cache) so users get the latest version that references correct versioned assets.


โ“ How do you handle cache invalidation?โ€‹

Answer: Three main approaches:

  1. Content hashing (best) - Change filename when content changes

    app.abc123.js โ†’ app.def456.js
  2. Versioned assets - Include version in path

    /v2/app.js โ†’ /v3/app.js
  3. Service worker versioning - Update cache name

    const CACHE_VERSION = 'v2';
    caches.open(CACHE_VERSION);

โ“ Difference between no-cache and no-store?โ€‹

DirectiveCaching BehaviorUse Case
no-cacheCaches but revalidates before useHTML, API responses that change
no-storeNever caches at allSensitive data (auth tokens, PII)

Example:

# Allows caching but requires validation
Cache-Control: no-cache

# Prevents all caching
Cache-Control: no-store

โ“ What improves LCP (Largest Contentful Paint) the most?โ€‹

Answer: For caching specifically:

  1. Cache static assets (JS, CSS, images) with long max-age
  2. Use CDN with edge caching for global users
  3. Implement immutable assets to eliminate revalidation requests
  4. Service worker for instant repeat visits
  5. Preload critical resources in HTML:
    <link rel="preload" href="hero.jpg" as="image">

Combined with proper caching headers, these dramatically reduce load times.


Here's a battle-tested caching configuration for production web applications:

# HTML files - Always revalidate
location ~* \.html$ {
add_header Cache-Control "no-cache";
}

# Versioned JS/CSS - Cache forever
location ~* \.[0-9a-f]{8,}\.(js|css)$ {
add_header Cache-Control "max-age=31536000, immutable";
}

# Images - Cache for 30 days
location ~* \.(jpg|jpeg|png|gif|svg|webp|ico)$ {
add_header Cache-Control "max-age=2592000, public";
}

# Fonts - Cache for 1 year
location ~* \.(woff|woff2|ttf|otf|eot)$ {
add_header Cache-Control "max-age=31536000, immutable";
}

# API endpoints - No caching or revalidate
location /api/ {
add_header Cache-Control "no-cache";
}

Summary Table:

ResourceCache-ControlDurationRationale
index.htmlno-cache0 (revalidate)Entry point; must be fresh
assets/*.jsmax-age=31536000, immutable1 yearContent-hashed; safe to cache
images/*max-age=259200030 daysBalance freshness and performance
api/*no-cache or no-store0 (revalidate)Dynamic data; freshness critical

Quick Reference Summaryโ€‹

One-Line Interview Answer:

"Cache aggressively where content is immutable, revalidate where freshness matters, and use service workers for full control."

Key Takeaways:

  1. Memory cache is fastest but volatile
  2. Disk cache persists across sessions
  3. Service workers provide programmable caching
  4. Cache-Control is the primary caching mechanism
  5. Content hashing solves cache invalidation
  6. HTML should use no-cache to stay fresh
  7. Static assets can cache for 1 year with immutable
  8. Stale-while-revalidate provides best UX
  9. CDN caching reduces origin load
  10. Different resources need different strategies

Cache Strategy Decision Tree:

Is content immutable (content-hashed)?
โ”œโ”€ Yes โ†’ max-age=1y, immutable
โ””โ”€ No โ†’ Does it change often?
โ”œโ”€ Yes โ†’ no-cache or no-store
โ””โ”€ No โ†’ max-age=30d (moderate)

Next Topicsโ€‹

Want to dive deeper? Consider exploring:

  • ๐Ÿ”ฅ Caching vs LocalStorage vs IndexedDB - Client-side storage comparison
  • ๐Ÿ”ฅ Browser Cache + React SSR (Next.js) - Server-side rendering caching strategies
  • ๐Ÿ”ฅ HTTP/2 Push and Caching - Interaction between server push and cache
  • ๐Ÿ”ฅ Cache Partitioning - Privacy-focused caching changes in modern browsers

Last Updated: December 2024