Caching Strategies
Caching is the process of storing copies of resources so they can be served faster on subsequent requests. It is one of the most effective performance optimizations — reducing load times from seconds to milliseconds by eliminating network round trips entirely.
Effective caching operates at multiple layers: the browser cache (HTTP), service workers (programmatic), CDN/edge caches (geographic distribution), and application caches (server-side). Each layer serves a different purpose and has different invalidation strategies.
HTTP cache headers control how browsers and proxies cache responses. Understanding these headers is fundamental to web performance — they determine whether a resource is fetched from cache or network on every visit.
| 1 | Cache-Control Header Directives: |
| 2 | |
| 3 | no-cache — Cache the response, but revalidate before every use |
| 4 | no-store — Never cache the response (always fetch from network) |
| 5 | max-age=N — Cache the response for N seconds |
| 6 | s-maxage=N — Cache for CDN/proxies for N seconds (overrides max-age) |
| 7 | must-revalidate — Once expired, must revalidate with server |
| 8 | immutable — Response will never change (skip revalidation entirely) |
| 9 | public — Any cache can store the response (browser, CDN, proxy) |
| 10 | private — Only the user's browser can cache (not CDN/proxy) |
| 11 | stale-while-revalidate=N — Serve stale cache, revalidate in background |
| 12 | stale-if-error=N — Serve stale cache if server returns error |
| 13 | |
| 14 | Common Cache-Control Patterns: |
| 15 | |
| 16 | # Static assets with content hash (immutable) |
| 17 | Cache-Control: public, max-age=31536000, immutable |
| 18 | |
| 19 | # HTML pages (always fresh) |
| 20 | Cache-Control: no-cache |
| 21 | |
| 22 | # API responses (never cache) |
| 23 | Cache-Control: no-store |
| 24 | |
| 25 | # Semi-dynamic content (revalidate frequently) |
| 26 | Cache-Control: public, max-age=60, stale-while-revalidate=3600 |
| 27 | |
| 28 | # User-specific content (private cache only) |
| 29 | Cache-Control: private, max-age=300 |
| 30 | |
| 31 | # CDN-cached content (long browser cache, long CDN cache) |
| 32 | Cache-Control: public, max-age=86400, s-maxage=604800 |
| 1 | // Node.js / Express — Setting cache headers |
| 2 | import express from "express"; |
| 3 | const app = express(); |
| 4 | |
| 5 | // Static assets with content hash (best for long-term caching) |
| 6 | app.use("/static", express.static("public", { |
| 7 | maxAge: "1y", |
| 8 | immutable: true, |
| 9 | setHeaders: (res, filePath) => { |
| 10 | if (filePath.endsWith(".html")) { |
| 11 | // HTML files should always be revalidated |
| 12 | res.setHeader("Cache-Control", "no-cache"); |
| 13 | } |
| 14 | }, |
| 15 | })); |
| 16 | |
| 17 | // API responses — stale-while-revalidate pattern |
| 18 | app.get("/api/products", (req, res) => { |
| 19 | res.setHeader("Cache-Control", "public, max-age=60, stale-while-revalidate=3600"); |
| 20 | res.setHeader("ETag", generateETag(products)); |
| 21 | res.json(products); |
| 22 | }); |
| 23 | |
| 24 | // User-specific content — private cache only |
| 25 | app.get("/api/dashboard", (req, res) => { |
| 26 | res.setHeader("Cache-Control", "private, max-age=300"); |
| 27 | res.json(getUserDashboard(req.user.id)); |
| 28 | }); |
| 29 | |
| 30 | // Never cache sensitive content |
| 31 | app.get("/api/account", (req, res) => { |
| 32 | res.setHeader("Cache-Control", "no-store"); |
| 33 | res.json(getAccountInfo(req.user.id)); |
| 34 | }); |
| 35 | |
| 36 | // ETag validation |
| 37 | function generateETag(data: any): string { |
| 38 | const hash = require("crypto") |
| 39 | .createHash("md5") |
| 40 | .update(JSON.stringify(data)) |
| 41 | .digest("hex"); |
| 42 | return `"${hash}"`; |
| 43 | } |
| 44 | |
| 45 | // Conditional responses (304 Not Modified) |
| 46 | app.get("/api/data", (req, res) => { |
| 47 | const data = getData(); |
| 48 | const etag = generateETag(data); |
| 49 | |
| 50 | if (req.headers["if-none-match"] === etag) { |
| 51 | return res.status(304).end(); |
| 52 | } |
| 53 | |
| 54 | res.setHeader("ETag", etag); |
| 55 | res.setHeader("Cache-Control", "private, max-age=0, must-revalidate"); |
| 56 | res.json(data); |
| 57 | }); |
| Resource Type | Cache-Control | Rationale |
|---|---|---|
| JS/CSS (hashed) | public, max-age=31536000, immutable | Content hash changes on update, safe to cache forever |
| Images (hashed) | public, max-age=31536000, immutable | Same as JS/CSS — content-addressable |
| HTML pages | no-cache | Always revalidate — entry point must be fresh |
| API (public) | public, max-age=60, s-maxage=300 | Short browser cache, longer CDN cache |
| API (private) | private, max-age=300 | User-specific data, browser only |
| Fonts | public, max-age=31536000, immutable | Rarely change, safe to cache long-term |
| Sensitive data | no-store | Never cache auth tokens, PII, financial data |
best practice
The stale-while-revalidate pattern serves cached content immediately while updating the cache in the background. Users get instant responses while fresh data is fetched asynchronously. This is the ideal pattern for semi-dynamic content.
| 1 | // stale-while-revalidate pattern in Service Worker |
| 2 | // sw-cache.js |
| 3 | const SWR_CACHE = "stale-while-revalidate-v1"; |
| 4 | const API_CACHE = "api-cache-v1"; |
| 5 | |
| 6 | self.addEventListener("fetch", (event) => { |
| 7 | const url = new URL(event.request.url); |
| 8 | |
| 9 | // Apply SWR to API requests |
| 10 | if (url.pathname.startsWith("/api/")) { |
| 11 | event.respondWith(swrFetch(event.request)); |
| 12 | return; |
| 13 | } |
| 14 | }); |
| 15 | |
| 16 | async function swrFetch(request) { |
| 17 | const cache = await caches.open(API_CACHE); |
| 18 | |
| 19 | // 1. Try to get cached response |
| 20 | const cachedResponse = await cache.match(request); |
| 21 | |
| 22 | // 2. Always fetch fresh data in background |
| 23 | const fetchPromise = fetch(request).then(async (networkResponse) => { |
| 24 | // 3. Update cache with fresh response |
| 25 | if (networkResponse.ok) { |
| 26 | await cache.put(request, networkResponse.clone()); |
| 27 | } |
| 28 | return networkResponse; |
| 29 | }).catch(() => { |
| 30 | // Network failed — return cached if available |
| 31 | return cachedResponse; |
| 32 | }); |
| 33 | |
| 34 | // 4. Return cached immediately if available, otherwise wait for network |
| 35 | return cachedResponse || fetchPromise; |
| 36 | } |
| 37 | |
| 38 | // More advanced: time-based SWR with max-age |
| 39 | async function timeAwareSWR(request) { |
| 40 | const cache = await caches.open(API_CACHE); |
| 41 | const cachedResponse = await cache.match(request); |
| 42 | |
| 43 | if (cachedResponse) { |
| 44 | const dateHeader = cachedResponse.headers.get("sw-cached-at"); |
| 45 | const cachedAt = dateHeader ? parseInt(dateHeader) : 0; |
| 46 | const age = Date.now() - cachedAt; |
| 47 | |
| 48 | // If cache is younger than max-age, return immediately |
| 49 | if (age < 60000) { // 60 seconds |
| 50 | // Revalidate in background if stale |
| 51 | if (age > 30000) { // 30 seconds |
| 52 | fetchAndCache(request, cache); |
| 53 | } |
| 54 | return cachedResponse; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | // Cache miss or expired — fetch from network |
| 59 | return fetchAndCache(request, cache); |
| 60 | } |
| 61 | |
| 62 | async function fetchAndCache(request, cache) { |
| 63 | try { |
| 64 | const response = await fetch(request); |
| 65 | if (response.ok) { |
| 66 | const responseToCache = response.clone(); |
| 67 | responseToCache.headers.set("sw-cached-at", Date.now().toString()); |
| 68 | await cache.put(request, responseToCache); |
| 69 | } |
| 70 | return response; |
| 71 | } catch (error) { |
| 72 | // Network failed, try stale cache |
| 73 | const stale = await cache.match(request); |
| 74 | if (stale) return stale; |
| 75 | throw error; |
| 76 | } |
| 77 | } |
pro tip
Service workers give you programmatic control over caching. They act as a proxy between the browser and network, intercepting requests and serving cached responses. This enables offline support, background updates, and fine-grained caching strategies.
| 1 | // Service Worker with multiple caching strategies |
| 2 | // sw.js |
| 3 | const STATIC_CACHE = "static-v1"; |
| 4 | const DYNAMIC_CACHE = "dynamic-v1"; |
| 5 | const IMAGE_CACHE = "images-v1"; |
| 6 | |
| 7 | const STATIC_ASSETS = [ |
| 8 | "/", |
| 9 | "/app.js", |
| 10 | "/styles.css", |
| 11 | "/offline.html", |
| 12 | ]; |
| 13 | |
| 14 | // Install — cache static assets |
| 15 | self.addEventListener("install", (event) => { |
| 16 | event.waitUntil( |
| 17 | caches.open(STATIC_CACHE).then((cache) => { |
| 18 | return cache.addAll(STATIC_ASSETS); |
| 19 | }) |
| 20 | ); |
| 21 | self.skipWaiting(); |
| 22 | }); |
| 23 | |
| 24 | // Activate — clean old caches |
| 25 | self.addEventListener("activate", (event) => { |
| 26 | event.waitUntil( |
| 27 | caches.keys().then((keys) => { |
| 28 | return Promise.all( |
| 29 | keys |
| 30 | .filter((key) => key !== STATIC_CACHE && key !== DYNAMIC_CACHE && key !== IMAGE_CACHE) |
| 31 | .map((key) => caches.delete(key)) |
| 32 | ); |
| 33 | }) |
| 34 | ); |
| 35 | self.clients.claim(); |
| 36 | }); |
| 37 | |
| 38 | // Fetch — routing to different caching strategies |
| 39 | self.addEventListener("fetch", (event) => { |
| 40 | const { request } = event; |
| 41 | const url = new URL(request.url); |
| 42 | |
| 43 | // Skip non-GET requests |
| 44 | if (request.method !== "GET") return; |
| 45 | |
| 46 | // Static assets — Cache First (immutable) |
| 47 | if (url.pathname.startsWith("/static/") || url.pathname.match(/\.\w{8}\.(js|css)$/)) { |
| 48 | event.respondWith(cacheFirst(request, STATIC_CACHE)); |
| 49 | return; |
| 50 | } |
| 51 | |
| 52 | // Images — Cache First with network fallback |
| 53 | if (request.destination === "image") { |
| 54 | event.respondWith(cacheFirst(request, IMAGE_CACHE)); |
| 55 | return; |
| 56 | } |
| 57 | |
| 58 | // API — Network First with cache fallback |
| 59 | if (url.pathname.startsWith("/api/")) { |
| 60 | event.respondWith(networkFirst(request, DYNAMIC_CACHE)); |
| 61 | return; |
| 62 | } |
| 63 | |
| 64 | // HTML pages — Network First with offline fallback |
| 65 | if (request.headers.get("accept")?.includes("text/html")) { |
| 66 | event.respondWith(networkFirst(request, DYNAMIC_CACHE, "/offline.html")); |
| 67 | return; |
| 68 | } |
| 69 | |
| 70 | // Everything else — Stale While Revalidate |
| 71 | event.respondWith(staleWhileRevalidate(request, DYNAMIC_CACHE)); |
| 72 | }); |
| 73 | |
| 74 | // Cache First — fastest, good for immutable assets |
| 75 | async function cacheFirst(request, cacheName) { |
| 76 | const cached = await caches.match(request); |
| 77 | if (cached) return cached; |
| 78 | |
| 79 | try { |
| 80 | const response = await fetch(request); |
| 81 | if (response.ok) { |
| 82 | const cache = await caches.open(cacheName); |
| 83 | cache.put(request, response.clone()); |
| 84 | } |
| 85 | return response; |
| 86 | } catch { |
| 87 | return new Response("Offline", { status: 503 }); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // Network First — best for dynamic content |
| 92 | async function networkFirst(request, cacheName, fallbackUrl) { |
| 93 | try { |
| 94 | const response = await fetch(request); |
| 95 | if (response.ok) { |
| 96 | const cache = await caches.open(cacheName); |
| 97 | cache.put(request, response.clone()); |
| 98 | } |
| 99 | return response; |
| 100 | } catch { |
| 101 | const cached = await caches.match(request); |
| 102 | if (cached) return cached; |
| 103 | if (fallbackUrl) { |
| 104 | return caches.match(fallbackUrl); |
| 105 | } |
| 106 | return new Response("Offline", { status: 503 }); |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | // Stale While Revalidate — balanced for semi-dynamic content |
| 111 | async function staleWhileRevalidate(request, cacheName) { |
| 112 | const cache = await caches.open(cacheName); |
| 113 | const cached = await cache.match(request); |
| 114 | |
| 115 | const fetchPromise = fetch(request).then((response) => { |
| 116 | if (response.ok) { |
| 117 | cache.put(request, response.clone()); |
| 118 | } |
| 119 | return response; |
| 120 | }).catch(() => cached); |
| 121 | |
| 122 | return cached || fetchPromise; |
| 123 | } |
| Strategy | When to Use | Latency | Freshness |
|---|---|---|---|
| Cache First | Static assets, images, fonts | Instant (cache hit) | Stale possible |
| Network First | API, HTML, dynamic data | Network latency | Always fresh |
| Stale While Revalidate | Semi-dynamic content, search | Instant (stale) + fresh | Eventually fresh |
| Network Only | Payments, auth, sensitive | Network only | Always fresh |
| Cache Only | Offline-first apps | Instant | Frozen in time |
info
Content Delivery Networks (CDNs) cache content at edge locations worldwide, reducing latency by serving resources from the nearest geographic location. CDN caching reduces TTFB, offloads your origin server, and improves reliability.
| 1 | # Cloudflare Cache Rules (via dashboard or API) |
| 2 | # Rule 1: Cache static assets for 1 year |
| 3 | # Match: *.js, *.css, *.woff2, *.webp, *.avif |
| 4 | # Cache-Control: public, max-age=31536000, immutable |
| 5 | |
| 6 | # Rule 2: Cache API responses for 5 minutes |
| 7 | # Match: /api/products/*, /api/categories/* |
| 8 | # Cache-Control: public, max-age=300, s-maxage=600 |
| 9 | |
| 10 | # Rule 3: Edge Caching with Purge |
| 11 | # Purge specific URLs after deploy |
| 12 | curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ |
| 13 | -H "Authorization: Bearer {api_token}" \ |
| 14 | -d '{"files":["https://example.com/app.js","https://example.com/styles.css"]}' |
| 15 | |
| 16 | # Vercel Edge Caching (via vercel.json) |
| 17 | # vercel.json |
| 18 | { |
| 19 | "headers": [ |
| 20 | { |
| 21 | "source": "/static/(.*)", |
| 22 | "headers": [ |
| 23 | { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } |
| 24 | ] |
| 25 | }, |
| 26 | { |
| 27 | "source": "/api/(.*)", |
| 28 | "headers": [ |
| 29 | { "key": "Cache-Control", "value": "public, s-maxage=60, stale-while-revalidate=300" } |
| 30 | ] |
| 31 | } |
| 32 | ] |
| 33 | } |
| 34 | |
| 35 | # Netlify Headers (_headers file) |
| 36 | # _headers |
| 37 | /static/* |
| 38 | Cache-Control: public, max-age=31536000, immutable |
| 39 | |
| 40 | /api/* |
| 41 | Cache-Control: public, s-maxage=60, stale-while-revalidate=300 |
| 42 | |
| 43 | /* |
| 44 | X-Content-Type-Options: nosniff |
warning
Cache invalidation is one of the hardest problems in computer science. The goal is to serve stale content as rarely as possible without unnecessary re-fetching. There are several proven strategies for different scenarios.
| 1 | // Strategy 1: Content hashing (best for static assets) |
| 2 | // Build output: app.a3f8b2c1.js, styles.e7d9f4b2.css |
| 3 | // HTML references hashed filenames — old URLs become invalid on deploy |
| 4 | |
| 5 | // In your build tool config: |
| 6 | // Webpack |
| 7 | output: { |
| 8 | filename: "[name].[contenthash:8].js", |
| 9 | chunkFilename: "[name].[contenthash:8].chunk.js", |
| 10 | } |
| 11 | |
| 12 | // Vite |
| 13 | build: { |
| 14 | rollupOptions: { |
| 15 | output: { |
| 16 | entryFileNames: "[name].[hash].js", |
| 17 | chunkFileNames: "chunks/[name].[hash].js", |
| 18 | }, |
| 19 | }, |
| 20 | } |
| 21 | |
| 22 | // Strategy 2: Version-based invalidation |
| 23 | // Append version query parameter to bust cache |
| 24 | const APP_VERSION = "2.1.0"; |
| 25 | const scriptUrl = `/app.js?v=${APP_VERSION}`; |
| 26 | |
| 27 | // Strategy 3: Cache tags (advanced CDN invalidation) |
| 28 | // Cloudflare Cache-Tag header |
| 29 | app.use("/static", express.static("public", { |
| 30 | setHeaders: (res, filePath) => { |
| 31 | if (filePath.endsWith(".js")) { |
| 32 | res.setHeader("Cache-Tag", "static,js,app"); |
| 33 | } |
| 34 | }, |
| 35 | })); |
| 36 | |
| 37 | // Purge by tag (Cloudflare) |
| 38 | fetch(`https://api.cloudflare.com/client/v4/zones/{zone}/purge_cache`, { |
| 39 | method: "POST", |
| 40 | headers: { "Authorization": "Bearer {token}" }, |
| 41 | body: JSON.stringify({ tags: ["static"] }), // Purge all "static" tagged resources |
| 42 | }); |
| 43 | |
| 44 | // Strategy 4: Service Worker update on deploy |
| 45 | // sw.js — check for updates on each page load |
| 46 | self.addEventListener("install", (event) => { |
| 47 | self.skipWaiting(); // Activate new SW immediately |
| 48 | }); |
| 49 | |
| 50 | self.addEventListener("activate", (event) => { |
| 51 | event.waitUntil( |
| 52 | caches.keys().then((keys) => { |
| 53 | return Promise.all( |
| 54 | keys.filter((key) => key !== CURRENT_CACHE).map((key) => caches.delete(key)) |
| 55 | ); |
| 56 | }) |
| 57 | ); |
| 58 | self.clients.claim(); // Take control of all pages immediately |
| 59 | }); |
| 60 | |
| 61 | // Notify users of updates |
| 62 | let newWorker; |
| 63 | navigator.serviceWorker.register("/sw.js").then((reg) => { |
| 64 | reg.addEventListener("updatefound", () => { |
| 65 | newWorker = reg.installing; |
| 66 | newWorker.addEventListener("statechange", () => { |
| 67 | if (newWorker.state === "activated") { |
| 68 | // Show update banner |
| 69 | showUpdateBanner(); |
| 70 | } |
| 71 | }); |
| 72 | }); |
| 73 | }); |
info
Next.js has a multi-layer caching system that can be confusing. Understanding each layer and how to control it is essential for building performant Next.js applications.
| 1 | // Next.js Caching Layers: |
| 2 | |
| 3 | // 1. Request Memoization (per-render, in-memory) |
| 4 | // Data fetched in React Server Components is memoized during render |
| 5 | // Automatic — no configuration needed |
| 6 | |
| 7 | // 2. Data Cache (persisted, across requests) |
| 8 | // Server-side cache for fetch() results, database queries, etc. |
| 9 | // Persists across deployments unless explicitly revalidated |
| 10 | |
| 11 | // Force no cache (always fetch fresh) |
| 12 | const data = await fetch("https://api.example.com/data", { |
| 13 | cache: "no-store", |
| 14 | }); |
| 15 | |
| 16 | // Cache with revalidation |
| 17 | const data = await fetch("https://api.example.com/data", { |
| 18 | next: { revalidate: 3600 }, // Revalidate every hour |
| 19 | }); |
| 20 | |
| 21 | // Cache forever (manual invalidation only) |
| 22 | const data = await fetch("https://api.example.com/data", { |
| 23 | next: { tags: ["products"] }, // Tag for manual revalidation |
| 24 | }); |
| 25 | |
| 26 | // 3. Full Route Cache (static rendering) |
| 27 | // Next.js caches the rendered HTML and RSC payload of static routes |
| 28 | // To opt out: use dynamic functions or export const dynamic = "force-dynamic" |
| 29 | |
| 30 | // 4. Router Cache (client-side, in-memory) |
| 31 | // Caches RSC payloads of visited routes in the browser |
| 32 | // Duration: session for static, 30 seconds for dynamic |
| 33 | |
| 34 | // On-demand revalidation |
| 35 | import { revalidateTag, revalidatePath } from "next/cache"; |
| 36 | |
| 37 | // Revalidate by tag |
| 38 | app.post("/api/revalidate", (req, res) => { |
| 39 | revalidateTag("products"); |
| 40 | res.json({ revalidated: true }); |
| 41 | }); |
| 42 | |
| 43 | // Revalidate by path |
| 44 | app.post("/api/revalidate-path", (req, res) => { |
| 45 | revalidatePath("/products"); |
| 46 | res.json({ revalidated: true }); |
| 47 | }); |
| 48 | |
| 49 | // Time-based revalidation in route handler |
| 50 | export const revalidate = 3600; // Revalidate this route every hour |
| 51 | |
| 52 | export async function GET() { |
| 53 | const data = await fetch("https://api.example.com/data", { |
| 54 | next: { revalidate: 3600 }, |
| 55 | }); |
| 56 | return Response.json(await data.json()); |
| 57 | } |
warning
Debugging caching issues requires checking multiple layers. A resource might be cached at the browser level, service worker level, CDN level, or server level — and each has different headers and invalidation behavior.
| 1 | # Check cache headers from the command line |
| 2 | curl -I https://example.com/app.js 2>/dev/null | grep -i "cache-control\|etag\|expires\|age\|x-cache" |
| 3 | |
| 4 | # Expected output for static assets: |
| 5 | # cache-control: public, max-age=31536000, immutable |
| 6 | # etag: "a3f8b2c1" |
| 7 | # x-cache: HIT (CDN cache hit) |
| 8 | |
| 9 | # Check if service worker is intercepting |
| 10 | # In Chrome DevTools > Application > Service Workers |
| 11 | # Or check via URL: chrome://serviceworker-internals/ |
| 12 | |
| 13 | # Clear browser cache (Cmd+Shift+R for hard reload) |
| 14 | # Clear service worker: Application > Service Workers > Unregister |
| 15 | |
| 16 | # Check cached resources |
| 17 | # Chrome DevTools > Application > Cache Storage |
| 18 | # Or: Chrome DevTools > Network > Size column (shows "from disk cache") |
| 19 | |
| 20 | # Test cache behavior |
| 21 | # 1. First load: check Network tab — status should be 200 |
| 22 | # 2. Reload: check Network tab — status should be 304 (ETag) or 200 (cache) |
| 23 | # 3. Check Age header — shows how long the resource has been cached at CDN |
| 24 | |
| 25 | # WebPageTest — test caching from multiple locations |
| 26 | # Run test with "Empty Cache" and "Repeat View" comparison |
| 27 | |
| 28 | # Lighthouse audit for caching |
| 29 | npx lighthouse https://example.com --only-categories=performance --output=json | \ |
| 30 | jq '.audits.uses-long-cache-ttl' |
| 31 |
| 1 | // Client-side cache debugging utility |
| 2 | function debugCache() { |
| 3 | const results: Record<string, any> = {}; |
| 4 | |
| 5 | // Check service worker status |
| 6 | if ("serviceWorker" in navigator) { |
| 7 | navigator.serviceWorker.getRegistrations().then((regs) => { |
| 8 | results.serviceWorkers = regs.map((reg) => ({ |
| 9 | scope: reg.scope, |
| 10 | active: !!reg.active, |
| 11 | installing: !!reg.installing, |
| 12 | waiting: !!reg.waiting, |
| 13 | })); |
| 14 | }); |
| 15 | } |
| 16 | |
| 17 | // Check cache storage |
| 18 | if ("caches" in window) { |
| 19 | caches.keys().then((keys) => { |
| 20 | results.cacheNames = keys; |
| 21 | return Promise.all( |
| 22 | keys.map((name) => |
| 23 | caches.open(name).then((cache) => |
| 24 | cache.keys().then((keys) => ({ |
| 25 | name, |
| 26 | count: keys.length, |
| 27 | urls: keys.map((req) => req.url), |
| 28 | })) |
| 29 | ) |
| 30 | ) |
| 31 | ); |
| 32 | }).then((cachesInfo) => { |
| 33 | results.caches = cachesInfo; |
| 34 | console.table(cachesInfo); |
| 35 | }); |
| 36 | } |
| 37 | |
| 38 | // Check resource timing for cache hits |
| 39 | const resources = performance.getEntriesByType("resource"); |
| 40 | const cacheStats = { |
| 41 | total: resources.length, |
| 42 | cached: resources.filter((r) => |
| 43 | (r as PerformanceResourceTiming).transferSize === 0 |
| 44 | ).length, |
| 45 | fromNetwork: resources.filter((r) => |
| 46 | (r as PerformanceResourceTiming).transferSize > 0 |
| 47 | ).length, |
| 48 | }; |
| 49 | |
| 50 | results.cacheStats = cacheStats; |
| 51 | console.log("Cache hit rate:", (cacheStats.cached / cacheStats.total * 100).toFixed(1) + "%"); |
| 52 | |
| 53 | return results; |
| 54 | } |