界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
设计与创意 · SKILL.md

frontend-browser-caching

frontend-browser-caching

【设计与创意】Use this skill when the user says 'browser caching', 'cache headers', 'service worker', 'SW', 'stale-while-revalidate', 'SWR', 'cache strategy', 'Cache-Control', 'ETag', 'cache busting', 'offline caching', or when optimizing frontend load performance. This skill enforces: Cache-Control headers with appropriate max-age, service worker caching with sta…

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: frontend-browser-caching description: > Use this skill when the user says 'browser caching', 'cache headers', 'service worker', 'SW', 'stale-while-revalidate', 'SWR', 'cache strategy', 'Cache-Control', 'ETag', 'cache busting', 'offline caching', or when optimizing frontend load performance. This skill enforces: Cache-Control headers with appropriate max-age, service worker caching with stale-while-revalidate pattern, cache-busted asset URLs, and offline fallback. Works with any frontend framework. Do NOT use for: backend response caching, CDN configuration, database query caching. version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [frontend, caching, performance, universal]

Browser Caching

Purpose

Optimize load performance via HTTP cache headers, service worker caching, and stale-while-revalidate. Minimize network round-trips while serving fresh content. First load under 2s, repeat loads under 500ms. Full offline support for static assets.

Agent Protocol

Trigger

Exact user phrases: "browser caching", "cache headers", "service worker", "SW", "stale-while-revalidate", "SWR", "cache strategy", "Cache-Control", "ETag", "cache busting", "offline caching".

Input Context

Before activating, verify:

  • Whether the focus is HTTP cache headers, service worker, or both.
  • The build tool (Vite, Webpack, Next.js) for cache-busting config.

Output Artifact

No file output. Produces caching strategy, header configs, or service worker code as text.

Response Format

Resource: {type}
Strategy: {cache strategy}
Config: {code block}

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output.

Completion Criteria

  • Static assets use Cache-Control: public, max-age=31536000, immutable.
  • HTML uses Cache-Control: no-cache for freshness validation.
  • API responses use appropriate Cache-Control or ETag.
  • Service worker registers and caches static assets on install.
  • Stale-while-revalidate pattern for data fetching.
  • Cache-busting via content hashes in filenames.

Max Response Length

4096 tokens.

Component Architecture / Decision Trees

Cache Strategy Decision Tree

Resource type?
  |-- Static asset (JS, CSS, image, font) -->
  |     |-- Content-hashed filename?
  |     |     |-- YES --> Cache-Control: public, max-age=31536000, immutable
  |     |     |-- NO  --> Add content hashing to build tool
  |-- HTML page -->
  |     |-- Static / pre-rendered? --> Cache-Control: public, max-age=300 (5 min), ETag
  |     |-- Dynamic / user-specific? --> Cache-Control: no-cache, private
  |-- API response -->
  |     |-- Public / shareable? --> Cache-Control: public, max-age=60, stale-while-revalidate=600
  |     |-- User-specific? --> Cache-Control: private, no-cache
  |-- Third-party resource -->
        |-- CDN-hosted? --> Use the CDN's cache headers, add integrity hash

Cache Location Decision Tree

Where should the response be cached?
  |-- Browser (private cache) -->
  |     |-- Static asset: Cache-Control: private, max-age=31536000
  |     |-- Auth response: Cache-Control: private, no-cache
  |-- CDN / proxy (shared cache) -->
  |     |-- Public page: Cache-Control: public, s-maxage=3600, max-age=60
  |     |-- API response: Cache-Control: public, s-maxage=300
  |-- Service worker -->
        |-- Install-time: Precache all versioned assets
        |-- Runtime: Stale-while-revalidate for API + HTML

Cache Invalidation Decision Tree

Content changed — how to invalidate?
  |-- Static asset changed -->
  |     |-- Content hash in URL? --> New URL = automatic invalidation
  |     |-- No hash? --> Versioned path (/v2/asset.js) or query string (?v=2)
  |-- API response changed -->
  |     |-- Use ETag? --> Conditional request validates freshness
  |     |-- No ETag? --> Short max-age or no-cache
  |-- HTML page changed -->
        |-- Use revalidation? --> no-cache + ETag = instant freshness check
        |-- Use CDN? --> Purge CDN cache for that path

Service Worker Architecture Options

Option A: Cache-first for static assets, network-first for HTML Best for: Content-heavy sites where assets change rarely. Provides offline capability.

Install: precache all static assets
Fetch: serve from cache, update in background (stale-while-revalidate)
HTML: network first, fallback to cache
API: stale-while-revalidate

Option B: Network-first with cache fallback Best for: Dynamic apps where freshness is critical (dashboards, admin panels).

Install: precache shell (minimal HTML, CSS, JS)
Fetch: HTML and API always from network
Static assets: cache-first
Fallback: cached offline page

Option C: Offline-first (fully cached) Best for: Progressive Web Apps that must work entirely offline.

Install: precache everything needed for full functionality
Fetch: cache-first for all resources, periodic background sync
API: IndexedDB local cache with background sync when online

Workflow

Step 1: HTTP Cache Headers by Resource Type

# Static assets (JS, CSS, images, fonts)
Cache-Control: public, max-age=31536000, immutable

# HTML pages
Cache-Control: no-cache
ETag: "abc123"

# API responses
Cache-Control: public, max-age=60, stale-while-revalidate=600

# User-specific content
Cache-Control: private, no-cache

Step 2: Service Worker Install & Cache

const CACHE_NAME = 'static-v1'
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/app.js',
  '/app.css',
  '/logo.png',
]

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
  )
  self.skipWaiting()
})

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
      )
    )
  )
  self.clients.claim()
})

Step 3: Stale-While-Revalidate Fetch Handler

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const fetchPromise = fetch(event.request).then((response) => {
        if (response.ok) {
          caches.open(CACHE_NAME).then((cache) => cache.put(event.request, response.clone()))
        }
        return response
      })
      return cached ?? fetchPromise
    })
  )
})

Step 4: Cache Busting with Content Hash

// Vite -- automatic content hashing
// build.rollupOptions.output.entryFileNames: '[name]-[hash].js'

// Webpack
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js',
}

// Import will resolve to cache-busted URL
import('/assets/app-abc123def.js')

Step 5: Cache Strategy Decision Table

ResourceStrategyCache-ControlSW Behavior
JS/CSS bundlesCache-firstmax-age=31536000, immutablePre-cache on install
ImagesCache-firstmax-age=31536000, immutableCache on first fetch
HTML pagesNetwork-firstno-cacheNetwork with fallback
API dataSWRmax-age=60, stale-while-revalidate=600Stale-while-revalidate
User dataNetwork-onlyprivate, no-cacheBypass cache

Step 6: Offline Fallback Page

<!-- offline.html -->
<!DOCTYPE html>
<html>
<head><title>Offline</title></head>
<body>
  <h1>You are offline</h1>
  <p>Please check your connection and try again.</p>
  <button onclick="location.reload()">Try again</button>
</body>
</html>
// In service worker fetch handler
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .catch(() => caches.match(event.request))
      .catch(() => caches.match('/offline.html'))
  )
})

Step 7: Workbox Configuration

// workbox-config.js
module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{js,css,png,jpg,svg,woff2}'],
  swDest: 'dist/sw.js',
  runtimeCaching: [
    {
      urlPattern: /\/api\//,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'api-cache',
        expiration: { maxEntries: 50, maxAgeSeconds: 86400 },
      },
    },
    {
      urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
      handler: 'CacheFirst',
      options: {
        cacheName: 'image-cache',
        expiration: { maxEntries: 100, maxAgeSeconds: 30 * 86400 },
      },
    },
  ],
}

Step 8: Browser Caching Architecture

[Browser Request]
     |
     v
[Service Worker] --(not registered)--> [HTTP Cache]
     |                                       |
     |-- Cache hit? --> Return cached         |-- max-age fresh? --> Return cached
     |-- No cache? --> Fetch from network     |-- no-cache? --> [Server] (conditional via ETag)
          |                                       |
          v                                       v
     [Network Response]                      [Network Response]
          |                                       |
          v                                       v
     [Cache Update]                          [Cache Update]

Step 9: Cache Partitioning

Modern browsers partition HTTP caches by site (top-level origin + frame origin). This means:

  • Cache keys include both the requesting site and the resource origin
  • A resource cached by site-a cannot be read by site-b even if same URL
  • Benefits: privacy (no cross-site cache snooping)
  • Tradeoffs: cache miss rate increases, more network requests
  • Mitigation: use service worker for shared resources (fonts, libraries) across same-origin apps

Step 10: Memory vs Disk Cache

  • Memory Cache: Fastest, cleared on tab close. Holds current page's resources.
  • Disk Cache: Persistent across sessions, slower than memory.
  • Browsers automatically decide which cache to use (memory = recently used, disk = everything else).
  • Service worker cache is separate from HTTP cache — always disk-backed.
  • To control: Service Worker Cache API gives full control. HTTP Cache-Control is advisory.

Common Pitfalls

1. Missing immutable Directive

Cache-Control: public, max-age=31536000 without immutable causes browsers to revalidate on every page reload (Cmd+R) even though the asset has a content hash. Always add immutable for content-hashed assets.

2. Caching HTML Without Revalidation

Setting max-age on HTML without ETag means users see stale content until the cache expires. Use no-cache with ETag for HTML to ensure freshness while still allowing conditional requests.

3. Over-caching User-Specific Content

Setting Cache-Control: public on authenticated API responses means a shared cache (CDN, proxy) serves the same response to all users. Always use private for user-specific data.

4. Service Worker Update Without Version Bump

If you update the service worker's cached assets but don't change the cache name (CACHE_NAME), the old cache is not cleaned up and new assets are never cached. Always bump the version in the cache name on each deploy.

// BAD -- same cache name, updates never applied
const CACHE_NAME = 'static-v1';

// GOOD -- versioned cache name
const CACHE_NAME = `static-v${new Date().getTime()}`;
// Or use a build-time constant
// const CACHE_NAME = `static-v${__CACHE_VERSION__}`;

5. Service Worker Blocking Updates

Without skipWaiting() in the install event, the new service worker waits for all tabs of the old service worker to close before activating. This delays updates indefinitely.

6. Cache Poisoning from Invalid Responses

The fetch handler caches ALL responses, including 404 and 500 errors. Only cache response.ok responses:

const fetchPromise = fetch(event.request).then((response) => {
  if (response.ok) {
    caches.open(CACHE_NAME).then((cache) => cache.put(event.request, response.clone()))
  }
  return response
})

7. Not Handling Opaque Responses

Cross-origin responses without CORS headers are "opaque" — they return status 0 and cannot be inspected with .ok. Cache them anyway for offline support:

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('cdn.example.com')) {
    event.respondWith(
      caches.match(event.request).then(cached => {
        const fetchPromise = fetch(event.request).then(response => {
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, response.clone()))
          return response
        })
        return cached || fetchPromise
      })
    )
  }
})

8. Exceeding Storage Quota

Each origin has limited cache storage (~6% of disk in Chrome). Beyond this, caches.put() throws a QuotaExceededError. Always set maximum cache sizes.

Compared With

StrategyFreshnessSpeedOfflineComplexity
Cache-first (immutable)Stale until expiryFastestYesLow
Network-firstAlways freshSlow (network dependent)YesMedium
Stale-while-revalidateStale served, fresh in backgroundVery fast (instant)YesMedium
Network-onlyAlways freshSlowestNoLow
Cache-onlyStale until expiryFastestYesLow
Service worker + IndexedDBBackground syncFast (local)FullHigh

Performance Considerations

Cache Hit Ratio Targets

  • Static assets: 99%+ cache hit ratio (content hashing ensures cacheable URLs)
  • HTML pages: 80%+ (ETag enables conditional revalidation)
  • API responses: 60-90% depending on data freshness requirements

Cache Size Management

Service worker caches can grow unbounded. Set a maximum cache size:

async function trimCache(cacheName, maxItems) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();
  if (keys.length > maxItems) {
    // Delete oldest entries
    const toDelete = keys.slice(0, keys.length - maxItems);
    await Promise.all(toDelete.map((key) => cache.delete(key)));
  }
}

// Call after adding to cache
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).then((response) => {
      if (response.ok) {
        const cloned = response.clone();
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, cloned);
          trimCache(CACHE_NAME, 50); // Keep max 50 cached entries
        });
      }
      return response;
    })
  );
});

Storage Quota Limits

Browsers limit storage per origin:

  • Chrome: ~60% of available disk space
  • Firefox: ~50% of available disk space
  • Safari: ~1GB (on iOS), ~500MB (on macOS)
  • Edge: same as Chrome

Use navigator.storage.estimate() to check available and used storage.

Performance Budget Targets

  • Initial load (cold cache): < 2s to interactive
  • Repeat load (warm cache): < 500ms to interactive
  • Cache hit ratio: > 95% for static assets
  • Service worker install: < 3s on 3G
  • Storage used: < 50MB for typical application

Browser Compatibility

  • Cache-Control immutable: Chrome, Firefox, Edge. Safari supports since iOS 16/macOS Ventura.
  • Service Worker: All modern browsers except IE11. Partial support in Samsung Internet.
  • Cache-Control stale-while-revalidate: Chrome, Firefox, Edge. Limited Safari support.
  • Private Cache-Control: All modern browsers.

Ecosystem & Tooling

Service Worker Libraries

  • Workbox -- Google's service worker library. High-level caching strategies, precaching, background sync, routing. Recommended for most projects.
  • Workbox Webpack Plugin -- Generates service worker config from Webpack build.
  • vite-plugin-pwa -- Vite plugin based on Workbox. Zero-config PWA setup.
  • next-pwa -- Next.js plugin for service worker generation.
  • sw-precache / sw-toolbox -- Legacy Workbox predecessors. Avoid in new projects.

CDN and Reverse Proxy

  • Cloudflare -- Cache rules, Argo Smart Routing, automatic static asset optimization.
  • Fastly -- Instant purge, VCL-based cache control.
  • Vercel Edge Network -- Automatic cache optimization for Next.js.
  • AWS CloudFront -- Lambda@Edge for custom caching logic.
  • Nginx -- expires and add_header Cache-Control directives.

Monitoring

  • Chrome DevTools > Network > Size column shows cache source (memory cache / disk cache / service worker / network)
  • navigator.storage.estimate() shows storage usage
  • Workbox generates debug logs in development mode
  • Lighthouse "uses efficient cache policy" audit

Security Considerations

  • Never cache responses with Set-Cookie headers in shared caches (CDN, proxy).
  • Use Cache-Control: private for authenticated content.
  • Service workers can intercept and modify requests — use SRI hashes on cached scripts to verify integrity.
  • Cache poisoning: validate response status before caching.
  • Opaque responses from CDNs may fail silently — test offline behavior.
  • Clear-Site-Data header can clear cache on logout.

Rules

  • Static assets: immutable + max-age=1 year + content hash in filename.
  • HTML: no-cache with ETag -- never cache HTML without revalidation.
  • Service worker: update version (cache name) on every deploy.
  • SWR: serve cached instantly, fetch fresh in background, update cache.
  • Never cache user-specific or sensitive data in shared caches.
  • Use Cache-Control: private for authenticated responses.
  • Always validate cache responses for status 200 before caching.
  • Cap cache size to prevent quota exceeded errors.
  • Use Workbox for production service workers (avoid raw SW code).
  • Register SW with updateViaCache: 'none' to prevent SW script caching.

References

  • references/cache-strategies.md -- Cache Strategies
  • references/caching-headers.md -- Caching Headers
  • references/caching-strategies.md -- Caching Strategies
  • references/local-storage-strategies.md -- Local Storage and Cache Strategies
  • references/service-worker-caching.md -- Service Worker Caching
  • references/sw-caching.md -- Service Worker Caching
  • references/service-worker-caching.md -- Service Worker Caching Patterns
  • references/cache-invalidation-strategies.md -- Cache Invalidation Strategies

Handoff

No artifact produced. Next skill: performance -- combine caching with other perf optimization. Carry forward: cache strategy per resource type, SW cache name version, hash config.

Implementation Patterns

Observer Pattern for Event Handling

` interface EventObserver { onEvent(event: T): Promise; }

class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `

Configuration-Driven Approach

config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1

Production Considerations

Deployment Checklist

  • Configuration validated against schema before startup
  • Health check endpoints registered and monitored
  • Graceful shutdown with draining period (30s timeout)
  • Resource limits configured (CPU, memory, file descriptors)
  • Log level set appropriate for environment
  • Metrics endpoint secured and exposed
  • Rate limiting configured per-tier
  • TLS certificates valid and auto-renewing
  • Database migrations run as separate deployment step
  • Feature flags ready for gradual rollout

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1% over 5minCriticalPage on-call
p99 latency> 2s over 5minWarningInvestigate
Throughput drop> 50% over 1minCriticalCheck upstream
Queue depth> 1000 over 1minWarningScale consumers
Disk usage> 85%WarningClean or expand
Memory usage> 90% heapCriticalRestart or scale

Anti-Patterns

Anti-PatternSymptomRoot CauseSolution
Premature optimizationComplex code for no measured benefitGuessing instead of profilingMeasure first, optimize based on data
Copy-paste reuseDuplicate code across codebaseLack of abstractionExtract shared logic into libraries
Gold-platingFeatures with no current requirementOver-engineeringYAGNI — build what's needed now
Magical thinkingAssumptions without validationSkipping error handlingHandle all failure modes explicitly

Performance Optimization

Caching Strategy

Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).

Resource Pooling

  • Database connections: Pool of reusable connections (HikariCP, pgBouncer)
  • HTTP connections: Keep-alive + connection pooling for external calls
  • Thread pool: Bounded thread pools for async task execution

Profiling Methodology

  1. Establish baseline with production traffic profile
  2. Profile CPU with sampling profiler (pprof, perf, async-profiler)
  3. Profile memory with heap dumps and allocation tracking
  4. Profile I/O with strace/perf trace for syscall analysis
  5. Profile latency with distributed tracing (OpenTelemetry)
  6. Identify bottleneck, formulate hypothesis, implement fix
  7. Re-profile to verify improvement, repeat

Architecture Decision Trees

Caching Strategy Decision Tree

What type of resource are you caching?
  ├── Static asset (JS, CSS, images) → Cache-First (Service Worker) + long max-age
  ├── API response → Network-First with stale-while-revalidate
  ├── HTML pages → Network-First with SW fallback to cache
  └── User data → Network-Only (never cache sensitive data)
       Need offline support?
       ├── Yes → Service Worker with Cache-First for critical paths
       └── No  → HTTP cache headers only (Cache-Control, ETag)

Cache Invalidation Decision Tree

Can the resource change without a version bump?
  ├── No  → Immutable content: Cache-Control: public, max-age=31536000, immutable
  └── Yes → Does it have a content hash?
       ├── Yes → Add hash as URL query or path param
       └── No  → Use ETag + conditional requests (304 Not Modified)
            Need real-time invalidation?
            ├── Yes → SW message channel + cache busting
            └── No  → Time-based TTL strategy
我的收藏