Browser Caching & Cache Policy Issues
What This Means
Browser caching allows web browsers to store copies of static resources (images, CSS, JavaScript, fonts) locally, so they don't need to be re-downloaded on subsequent visits. Poor cache configuration forces browsers to re-download resources unnecessarily, wasting bandwidth and slowing down repeat visits.
Cache Policy Types
Cache-Control Directives:
max-age=31536000- Cache for 1 year (recommended for static assets)no-cache- Validate with server before using cached versionno-store- Never cache (sensitive data)public- Can be cached by browsers and CDNsprivate- Only browser can cache (not CDNs)immutable- Resource will never change (perfect for hashed filenames)
Common Cache Durations:
- Static assets (versioned): 1 year (31536000 seconds)
- Images: 1 week to 1 year (604800-31536000 seconds)
- HTML: 0 seconds to 1 hour (0-3600 seconds)
- API responses: Varies (often no-cache or short duration)
Impact on Your Business
Performance Impact:
- Poor caching = slower repeat visits
- Every resource re-downloaded wastes time
- Mobile users especially affected (limited data)
- Increased server load and bandwidth costs
User Experience:
- Fast initial load but slow repeat visits
- Unnecessary loading indicators
- Wasted mobile data
- Poor perceived performance
Business Costs:
- Higher bandwidth bills
- Increased CDN costs
- More server resources needed
- Reduced conversion on repeat visits
- Improves LCP on repeat visits
- Reduces Time to First Byte (TTFB)
- Better First Contentful Paint (FCP)
- Overall better performance scores
How to Diagnose
Method 1: Chrome DevTools Network Panel (Recommended)
- Open Chrome DevTools (
F12) - Navigate to "Network" tab
- Check "Disable cache" is UNCHECKED
- Reload page (first visit)
- Reload page again (second visit)
- Look at "Size" column:
(disk cache)or(memory cache)= cached properly- File size (e.g., "45.2 KB") = not cached, re-downloaded
Advanced Analysis:
- Click on any resource
- View "Headers" tab
- Check "Response Headers" for:
cache-control: max-age=31536000 - Check "Request Headers" for:
if-modified-since: [date] if-none-match: [etag]
What to Look For:
- Resources downloading on second visit (should be cached)
- Missing or short
max-agevalues no-cacheorno-storeon static resources- No
cache-controlheader at all - 200 status (re-download) instead of 304 (not modified)
Method 2: Lighthouse Audit
- Open Chrome DevTools (
F12) - Navigate to "Lighthouse" tab
- Run Performance audit
- Look for "Serve static assets with an efficient cache policy"
What to Look For:
- List of resources with poor cache policies
- Recommended cache duration for each
- Potential savings in bytes and requests
- Number of resources affected
Method 3: PageSpeed Insights
- Visit PageSpeed Insights
- Enter your URL
- Review diagnostics section
- Look for "Serve static assets with an efficient cache policy"
What to Look For:
- Resources with short cache duration
- Total size of resources not cached properly
- Specific URLs and recommended cache times
- Mobile vs desktop differences
Method 4: WebPageTest
- Visit WebPageTest.org
- Enter your URL
- Run test
- Look at:
- "First View" vs "Repeat View" performance
- Large difference indicates caching issues
- Content breakdown showing cache headers
What to Look For:
- Minimal improvement from first to repeat view
- Resources downloading on repeat view
- Missing cache headers
- Grade for "Cache static content"
Method 5: Manual Header Check
Use curl to check cache headers:
# Check cache headers for an image
curl -I https://example.com/image.jpg
# Look for:
# Cache-Control: max-age=31536000
# ETag: "abc123"
# Last-Modified: [date]
What to Look For:
- Presence of Cache-Control header
- Appropriate max-age value
- ETag or Last-Modified for validation
- Correct directives (public vs private)
General Fixes
Fix 1: Set Long Cache for Static Assets
Configure proper cache headers:
Apache (.htaccess):
<IfModule mod_expires.c> ExpiresActive On # Images ExpiresByType image/jpeg "access plus 1 year" ExpiresByType image/png "access plus 1 year" ExpiresByType image/gif "access plus 1 year" ExpiresByType image/webp "access plus 1 year" ExpiresByType image/svg+xml "access plus 1 year" # CSS and JavaScript ExpiresByType text/css "access plus 1 year" ExpiresByType text/javascript "access plus 1 year" ExpiresByType application/javascript "access plus 1 year" # Fonts ExpiresByType font/woff "access plus 1 year" ExpiresByType font/woff2 "access plus 1 year" ExpiresByType application/font-woff "access plus 1 year" # HTML - short cache ExpiresByType text/html "access plus 0 seconds" </IfModule> # Cache-Control headers <IfModule mod_headers.c> <FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|svg|css|js|woff|woff2)$"> Header set Cache-Control "public, max-age=31536000, immutable" </FilesMatch> <FilesMatch "\.(html|htm)$"> Header set Cache-Control "no-cache, must-revalidate" </FilesMatch> </IfModule>Nginx (nginx.conf):
# Cache static assets location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2)$ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; } # HTML - no cache location ~* \.(html|htm)$ { expires -1; add_header Cache-Control "no-cache, must-revalidate"; } # API responses - no cache location /api/ { add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"; }Express.js (Node.js):
const express = require('express'); const app = express(); // Serve static files with cache app.use('/static', express.static('public', { maxAge: '1y', immutable: true })); // HTML files - no cache app.get('*.html', (req, res) => { res.set('Cache-Control', 'no-cache, must-revalidate'); res.sendFile(__dirname + req.path); });Next.js (next.config.js):
module.exports = { async headers() { return [ { source: '/static/:path*', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable', }, ], }, { source: '/:path*.html', headers: [ { key: 'Cache-Control', value: 'no-cache, must-revalidate', }, ], }, ]; }, };
Fix 2: Implement Cache Busting
Ensure users get updates when files change:
Versioned filenames (recommended):
<!-- Build tools add hash to filename --> <link rel="stylesheet" href="styles.a8f3d2e1.css"> <script src="app.b7c4e9f2.js"></script>Build configuration (Webpack):
module.exports = { output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js' } };Query string versioning:
<!-- Add version to query string --> <link rel="stylesheet" href="styles.css?v=1.2.3"> <script src="app.js?v=1.2.3"></script>Automatic versioning with build tools:
// Vite automatically adds hashes import './style.css'; // Becomes style.abc123.css in build
Fix 3: Use ETags and Validation
Allow conditional requests:
Apache - Enable ETags:
# .htaccess FileETag MTime Size <IfModule mod_headers.c> Header set ETag "%{REQUEST_TIME}x-%{REQUEST_SIZE}x" </IfModule>Nginx - Enable ETags:
# ETags enabled by default # Ensure not disabled etag on;Handle 304 Not Modified responses:
// Express.js example app.get('/data.json', (req, res) => { const etag = generateETag(data); if (req.headers['if-none-match'] === etag) { res.status(304).end(); return; } res.set('ETag', etag); res.set('Cache-Control', 'public, max-age=3600'); res.json(data); });
Fix 4: Optimize Cache Strategy by Resource Type
Different resources need different strategies:
Static assets (versioned filenames):
Cache-Control: public, max-age=31536000, immutableImages without versioning:
Cache-Control: public, max-age=2592000 (30 days)HTML pages:
Cache-Control: no-cache (always validate with server)API responses (data that changes):
Cache-Control: no-cache, must-revalidateUser-specific content:
Cache-Control: private, max-age=3600 (cache in browser only, not CDNs)Sensitive data:
Cache-Control: no-store (never cache)
Fix 5: Leverage CDN Caching
CDN can serve cached content globally:
Set appropriate cache headers for CDN:
Cache-Control: public, max-age=31536000 CDN-Cache-Control: max-age=86400Use CDN features:
- Enable automatic cache headers
- Set up cache purge/invalidation
- Configure cache keys properly
- Use edge caching for API responses
Cloudflare example:
// Cloudflare Workers addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { const cache = caches.default; let response = await cache.match(request); if (!response) { response = await fetch(request); response = new Response(response.body, response); response.headers.set('Cache-Control', 'public, max-age=86400'); event.waitUntil(cache.put(request, response.clone())); } return response; }
Fix 6: Implement Service Worker Caching
Advanced caching with Service Workers:
Cache static assets:
// service-worker.js const CACHE_NAME = 'v1'; const STATIC_ASSETS = [ '/', '/styles.css', '/app.js', '/logo.png' ]; // Install - cache static assets self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return cache.addAll(STATIC_ASSETS); }) ); }); // Fetch - serve from cache, fallback to network self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });Network first with cache fallback:
self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(response => { // Update cache with fresh response const responseClone = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseClone); }); return response; }) .catch(() => { // Network failed, try cache return caches.match(event.request); }) ); });Stale-while-revalidate:
self.addEventListener('fetch', event => { event.respondWith( caches.open(CACHE_NAME).then(cache => { return cache.match(event.request).then(response => { const fetchPromise = fetch(event.request).then(networkResponse => { cache.put(event.request, networkResponse.clone()); return networkResponse; }); // Return cached version immediately, update in background return response || fetchPromise; }); }) ); });
Fix 7: Avoid Common Caching Mistakes
Don't cache what shouldn't be cached:
Don't cache HTML too long:
✗ Cache-Control: max-age=31536000 ✓ Cache-Control: no-cacheDon't cache user-specific content publicly:
✗ Cache-Control: public, max-age=3600 ✓ Cache-Control: private, max-age=3600Don't cache without validation for dynamic content:
✗ Cache-Control: max-age=86400 ✓ Cache-Control: max-age=86400, must-revalidateDon't forget to version static assets:
✗ <link rel="stylesheet" href="styles.css"> ✓ <link rel="stylesheet" href="styles.v2.css"> ✓ <link rel="stylesheet" href="styles.abc123.css">
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
| Platform | Troubleshooting Guide |
|---|---|
| Shopify | Shopify Caching Guide |
| WordPress | WordPress Caching Guide |
| Wix | Wix Caching Guide |
| Squarespace | Squarespace Caching Guide |
| Webflow | Webflow Caching Guide |
Verification
After implementing caching:
Test in Chrome DevTools:
- Load page fresh (hard reload: Cmd/Ctrl + Shift + R)
- Reload normally (Cmd/Ctrl + R)
- Check Network tab for "(disk cache)"
- Verify response headers show correct cache-control
Run Lighthouse:
- Should pass "Serve static assets with an efficient cache policy"
- Check potential savings (should be minimal)
Test cache busting:
- Update a CSS or JS file
- Deploy with new version/hash
- Verify users get new version
- Old version still cached
Test repeat visits:
- Use WebPageTest.org
- Compare "First View" vs "Repeat View"
- Repeat view should be significantly faster
- Most resources from cache
Monitor in production:
- Check CDN cache hit rates
- Monitor bandwidth usage (should decrease)
- Check server load (should decrease)
- Verify real user performance improves
Common Mistakes
- Caching HTML too long - Users don't get updates
- Not versioning static files - Can't change cached files
- Using no-cache on static assets - Defeats purpose
- Inconsistent cache headers - Different headers for same file
- Forgetting mobile users - Poor caching wastes mobile data
- Not testing cache behavior - Assumes it works without verification
- Caching errors - 404 or 500 pages cached
- Ignoring CDN cache - Only focusing on browser cache
- No cache invalidation strategy - Can't force updates when needed
- Caching user-specific content publicly - Privacy and correctness issues