Browser Caching - Complete Guide
Table of Contentsโ
- Why Browser Caching Matters
- Types of Browser Caches
- HTTP Cache-Control
- ETag vs Last-Modified
- Versioned Asset Caching
- HTML vs Static Assets Caching
- Service Worker Caching Strategies
- CDN Caching
- API Response Caching
- Common Interview Questions
- Recommended Production Setup
- 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:
- Browser checks cache first
- If valid, serves immediately
- No network request made
- 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.
| Header | Description | How It Works | Limitations |
|---|---|---|---|
| ETag | Hash or version of resource | Server generates hash of content; browser sends If-None-Match | Multi-node servers may generate different ETags |
| Last-Modified | Timestamp of last change | Server sends modification time; browser sends If-Modified-Since | Second-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:
- Build tools (Webpack, Vite, etc.) generate hashed filenames
- HTML references these hashed files
- Browser caches them aggressively (1 year)
- 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 Type | Strategy | Header | Reason |
|---|---|---|---|
| HTML | Revalidate | Cache-Control: no-cache | HTML references other assets; stale HTML breaks deployments |
| JS/CSS | Strong cache | Cache-Control: max-age=31536000, immutable | Versioned filenames enable safe long-term caching |
| Images | Moderate cache | Cache-Control: max-age=2592000 (30 days) | Less frequently updated; balance between freshness and performance |
| API | Revalidate or no-store | Cache-Control: no-cache or no-store | Data 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:
| Directive | Meaning | Duration |
|---|---|---|
public | Response can be cached by CDN (not just browser) | - |
max-age=600 | Browser cache time | 10 minutes |
s-maxage=3600 | Shared/CDN cache time | 1 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:
- First user request โ CDN fetches from origin, caches for 1 hour
- Next user request โ CDN serves from edge cache (very fast)
- After 1 hour โ CDN revalidates with origin
- 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:
| Mode | Behavior |
|---|---|
default | Browser default behavior (respects Cache-Control) |
no-store | Never cache, always fetch fresh |
reload | Always fetch from network, update cache |
no-cache | Validate before using cache (conditional request) |
force-cache | Use cache even if stale (offline-first) |
only-if-cached | Use 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:
- User gets old HTML from cache (references old
app.v1.js) - But
app.v1.jsmight be deleted from server - 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:
-
Content hashing (best) - Change filename when content changes
app.abc123.js โ app.def456.js -
Versioned assets - Include version in path
/v2/app.js โ /v3/app.js -
Service worker versioning - Update cache name
const CACHE_VERSION = 'v2';
caches.open(CACHE_VERSION);
โ Difference between no-cache and no-store?โ
| Directive | Caching Behavior | Use Case |
|---|---|---|
no-cache | Caches but revalidates before use | HTML, API responses that change |
no-store | Never caches at all | Sensitive 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:
- Cache static assets (JS, CSS, images) with long max-age
- Use CDN with edge caching for global users
- Implement immutable assets to eliminate revalidation requests
- Service worker for instant repeat visits
- Preload critical resources in HTML:
<link rel="preload" href="hero.jpg" as="image">
Combined with proper caching headers, these dramatically reduce load times.
Recommended Production Setupโ
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:
| Resource | Cache-Control | Duration | Rationale |
|---|---|---|---|
index.html | no-cache | 0 (revalidate) | Entry point; must be fresh |
assets/*.js | max-age=31536000, immutable | 1 year | Content-hashed; safe to cache |
images/* | max-age=2592000 | 30 days | Balance freshness and performance |
api/* | no-cache or no-store | 0 (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:
- Memory cache is fastest but volatile
- Disk cache persists across sessions
- Service workers provide programmable caching
Cache-Controlis the primary caching mechanism- Content hashing solves cache invalidation
- HTML should use
no-cacheto stay fresh - Static assets can cache for 1 year with immutable
- Stale-while-revalidate provides best UX
- CDN caching reduces origin load
- 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