Skip to main content
CodeRocket
Performancehighcaching

Enable browser caching

rule · browser-caching

Browser caching stores resources locally to eliminate repeated downloads.

Code Example

JavaScript
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        // Hashed static assets
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // Images
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=2592000, stale-while-revalidate=86400',
          },
        ],
      },
      {
        // HTML pages - always revalidate
        source: '/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=0, must-revalidate',
          },
        ],
      },
    ]
  },
}

Why It Matters

Proper caching eliminates redundant downloads on repeat visits—returning users see near-instant page loads when assets are served from browser cache.

Cache Header Strategy

Resource TypeCache-ControlDuration
Hashed assets (CSS, JS)public, max-age=31536000, immutable1 year
Imagespublic, max-age=259200030 days
Fontspublic, max-age=31536000, immutable1 year
HTML pagespublic, max-age=0, must-revalidateAlways check
API responsesprivate, max-age=0, no-storeNo cache

Cache-Control Directives

DirectivePurpose
publicCan be cached by browsers and CDNs
privateOnly browser can cache (sensitive data)
max-age=NCache valid for N seconds
immutableResource won't change (use with hashed filenames)
must-revalidateCheck with server when stale
no-cacheAlways revalidate before using
no-storeNever cache (sensitive data)
stale-while-revalidate=NServe stale, fetch fresh in background

Nginx Configuration

NGINX
# Static assets with hash in filename
location ~* \.[a-f0-9]{8,}\.(js|css)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}
 
# Images
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, stale-while-revalidate=86400";
}
 
# Fonts
location ~* \.(woff2?|ttf|otf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}
 
# HTML - always revalidate
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "public, max-age=0, must-revalidate";
}

Express.js Configuration

JavaScript
import express from 'express'
 
const app = express()
 
// Serve static files with caching
app.use('/static', express.static('public', {
  maxAge: '1y',
  immutable: true,
  etag: true,
  lastModified: true,
}))
 
// Custom middleware for fine-grained control
app.use((req, res, next) => {
  // Set cache headers based on file type
  const ext = req.path.split('.').pop()
 
  if (['js', 'css'].includes(ext) && req.path.includes('.')) {
    res.set('Cache-Control', 'public, max-age=31536000, immutable')
  } else if (['jpg', 'png', 'webp', 'svg'].includes(ext)) {
    res.set('Cache-Control', 'public, max-age=2592000')
  }
 
  next()
})

ETags for Cache Validation

JavaScript
// Express.js ETag configuration
import express from 'express'
 
const app = express()
 
// Enable strong ETags
app.set('etag', 'strong')
 
// How ETags work:
// 1. Server sends: ETag: "abc123"
// 2. Browser caches response
// 3. On next request, browser sends: If-None-Match: "abc123"
// 4. If resource unchanged, server responds: 304 Not Modified
// 5. Browser uses cached version (no body transferred)

Service Worker Caching

JavaScript
// sw.js - Advanced caching strategies
const CACHE_NAME = 'v1'
const STATIC_ASSETS = ['/app.js', '/styles.css', '/logo.svg']
 
// Cache on install
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  )
})
 
// Stale-while-revalidate strategy
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      const fetchPromise = fetch(event.request).then(response => {
        // Update cache
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, response.clone())
        })
        return response
      })
 
      // Return cached immediately, update in background
      return cached || fetchPromise
    })
  )
})

Verifying Cache Headers

JavaScript
// Check cache headers in browser
async function verifyCaching(url) {
  const response = await fetch(url, { method: 'HEAD' })
 
  const headers = {
    'cache-control': response.headers.get('cache-control'),
    'etag': response.headers.get('etag'),
    'last-modified': response.headers.get('last-modified'),
    'expires': response.headers.get('expires'),
  }
 
  console.table(headers)
  return headers
}
 
// In DevTools Network tab:
// - Size column shows "(disk cache)" or "(memory cache)" for cached resources
// - Time column shows fast load times for cached resources

Standards

  • Use web.dev: Learn Performance as the standard for measuring the final production behavior, not just local synthetic output.
  • Use Chrome Developers: Lighthouse overview as the standard for measuring the final production behavior, not just local synthetic output.

Support Notes

  • Caching behavior depends on real cache headers, intermediary caches, and revalidation paths, so confirm the final network behavior in production-like conditions.
  • Do not assume origin configuration equals browser behavior when a CDN or service worker can change the effective caching path.

Verification

Automated Checks

  • Check Network tab—look for "(disk cache)" or "(memory cache)"
  • Verify Cache-Control headers in response
  • Use WebPageTest to compare first vs repeat view

Manual Checks

  • Test repeat visits—assets should load instantly
  • Check service worker caching in Application tab