Chiến lược Caching Web 2026: Cache-Control, Service Worker, CDN và stale-while-revalidate

Hướng dẫn chi tiết chiến lược caching web 2026 — từ HTTP Cache-Control, Service Worker Workbox, CDN edge caching đến stale-while-revalidate. Code ví dụ thực tế và bảng cấu hình cho từng loại tài nguyên.

Giới thiệu: Caching — Vũ khí bí mật mà nhiều developer bỏ quên

Nếu bạn đã theo dõi chuỗi bài về Core Web Vitals của mình — từ tối ưu hình ảnh, script bên thứ ba, font web đến JavaScript bundle — thì đây là phần thứ năm. Và nói thật nhé, đây là phần mà mình thấy nhiều developer bỏ qua nhất.

Bạn có thể nén ảnh xuống còn vài chục KB, code split JavaScript hoàn hảo, nhưng nếu không có chiến lược caching đúng đắn? Người dùng vẫn phải tải lại tất cả mỗi lần quay lại trang. Nghe hơi vô lý đúng không?

Theo dữ liệu HTTP Archive đầu năm 2026, hơn 42% website vẫn không thiết lập Cache-Control header hợp lý cho tài nguyên tĩnh. Hàng triệu lượt truy cập lặp lại vẫn phải tải lại CSS, JavaScript, và font từ đầu — lãng phí băng thông, đẩy TTFB lên cao, và kéo điểm LCP xuống đáng kể.

Caching không chỉ là "set max-age rồi quên đi". Năm 2026, với HTTP cache partitioning trong Chrome, Service Worker ngày càng mạnh, và CDN edge computing phổ biến hơn bao giờ hết, bạn cần một chiến lược caching có hệ thống. Bạn phải hiểu rõ từng layer, biết khi nào dùng immutable, khi nào cần stale-while-revalidate, và tại sao Service Worker có thể thay đổi cuộc chơi.

Trong bài này, mình sẽ đi chi tiết từng tầng caching: HTTP Cache-Control headers, Service Worker với Workbox, CDN edge caching, và cách kết hợp tất cả để tối ưu Core Web Vitals. Code ví dụ chạy được, cấu hình thực tế — không lý thuyết suông đâu.

1. Hiểu các tầng caching trong web hiện đại

1.1 Ba tầng cache và thứ tự ưu tiên

Khi trình duyệt yêu cầu một tài nguyên, nó kiểm tra cache theo thứ tự sau:

  1. Service Worker Cache (nếu có): Service Worker chặn request trước tiên, quyết định trả từ cache hay chuyển tiếp ra mạng
  2. HTTP Browser Cache: Nếu Service Worker không xử lý (hoặc không có), trình duyệt kiểm tra HTTP cache dựa trên Cache-Control headers
  3. CDN Edge Cache: Nếu không có trong browser cache, request đi ra mạng — và CDN sẽ check edge cache trước khi chuyển về origin server

Mỗi tầng có ưu và nhược điểm riêng. Hiểu rõ cách chúng hoạt động với nhau là nền tảng để xây dựng chiến lược caching hiệu quả.

1.2 Caching ảnh hưởng đến Core Web Vitals như thế nào?

Caching tác động trực tiếp đến khá nhiều chỉ số Core Web Vitals:

  • LCP (Largest Contentful Paint): Tải tài nguyên từ cache nhanh hơn hàng chục lần so với network. Một hình ảnh LCP đã cache có thể hiển thị trong vài millisecond thay vì vài giây — chênh lệch khổng lồ
  • TTFB (Time to First Byte): CDN edge cache có thể giảm TTFB từ 800ms xuống dưới 50ms. Cứ tưởng tượng trang web của bạn phản hồi nhanh gấp 16 lần
  • INP (Interaction to Next Paint): Service Worker phục vụ tài nguyên từ cache giúp giảm tải cho main thread, gián tiếp cải thiện khả năng phản hồi tương tác
  • CLS (Cumulative Layout Shift): Font và hình ảnh đã cache sẽ hiển thị ngay, tránh layout shift do tải chậm

2. HTTP Cache-Control — Nền tảng của mọi chiến lược caching

2.1 Các directive quan trọng nhất

Header Cache-Control là "bộ điều khiển" chính cho HTTP caching. Mình hay gọi nó là "remote control" cho cache — bạn ra lệnh, trình duyệt và CDN nghe theo. Dưới đây là các directive bạn cần nắm vững:

# Các directive Cache-Control phổ biến

# public: Cho phép mọi cache (browser, CDN, proxy) lưu trữ
Cache-Control: public

# private: Chỉ browser cache, không cho CDN/proxy
Cache-Control: private

# max-age: Thời gian (giây) tài nguyên được coi là "fresh"
Cache-Control: max-age=3600

# s-maxage: Giống max-age nhưng chỉ áp dụng cho shared cache (CDN/proxy)
# Override max-age ở CDN level
Cache-Control: s-maxage=600

# immutable: Tài nguyên không bao giờ thay đổi — browser không cần revalidate
Cache-Control: immutable

# no-cache: VẪN cache, nhưng phải revalidate mỗi lần dùng
# (Nhiều người nhầm "no-cache" = "không cache" — SAI!)
Cache-Control: no-cache

# no-store: KHÔNG cache — dùng cho dữ liệu nhạy cảm
Cache-Control: no-store

# must-revalidate: Khi hết hạn, BẮT BUỘC phải revalidate
Cache-Control: must-revalidate

# stale-while-revalidate: Cho phép dùng cache cũ trong khi revalidate ngầm
Cache-Control: stale-while-revalidate=60

2.2 Bảng cấu hình Cache-Control theo loại tài nguyên

Đây là bảng tham chiếu nhanh mà mình dùng cho hầu hết các dự án. Bạn có thể copy và điều chỉnh theo nhu cầu:

# === NGINX Configuration ===

# JS/CSS có hash trong tên file (app.9f2d1a.js)
# Cache 1 năm, không revalidate — immutable
location ~* \.(js|css)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# Hình ảnh tĩnh
# Cache 1 ngày, stale-while-revalidate 1 giờ
location ~* \.(png|jpg|jpeg|webp|avif|svg|gif|ico)$ {
    add_header Cache-Control "public, max-age=86400, stale-while-revalidate=3600";
}

# Font files
# Cache 1 năm — font hiếm khi thay đổi
location ~* \.(woff2|woff|ttf|eot)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML pages
# Cache ngắn ở browser (1 phút), dài hơn ở CDN (5 phút)
# Dùng stale-while-revalidate để UX mượt mà
location ~* \.html$ {
    add_header Cache-Control "public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600";
}

# API responses (public)
# Không cache ở browser, cache 1 phút ở CDN
location /api/public/ {
    add_header Cache-Control "public, max-age=0, s-maxage=60, stale-while-revalidate=30";
}

# API responses (private/authenticated)
# KHÔNG BAO GIỜ cache
location /api/user/ {
    add_header Cache-Control "private, no-store";
}

2.3 immutable — Directive mà 90% developer chưa dùng

Thật sự mà nói, đây là một trong những directive ít được biết đến nhưng cực kỳ hiệu quả. Khi bạn dùng fingerprinting cho tài nguyên tĩnh (tên file chứa hash như app.9f2d1a.js), file sẽ không bao giờ thay đổi — nếu nội dung thay đổi, hash thay đổi, URL mới được tạo ra.

Vấn đề nằm ở chỗ: dù bạn đã set max-age=31536000 (1 năm), trình duyệt vẫn có thể gửi conditional request (với If-None-Match) khi người dùng reload trang. Server trả về 304 Not Modified — nhanh hơn tải lại toàn bộ, nhưng vẫn tốn một round-trip.

Thêm immutable sẽ bảo trình duyệt: "Đừng bận tâm revalidate, file này không thay đổi đâu." Kết quả? Zero requests, kể cả khi người dùng nhấn Ctrl+R.

Tiết kiệm được hàng chục đến hàng trăm millisecond cho mỗi tài nguyên — nghe có vẻ ít, nhưng nhân lên vài chục file thì con số rất đáng kể.

# Ví dụ: Vite tự động thêm hash vào tên file
# dist/assets/index-BhFg2k1A.js
# dist/assets/index-DiwrgTda.css

# Cấu hình hoàn hảo cho fingerprinted assets:
Cache-Control: public, max-age=31536000, immutable

2.4 stale-while-revalidate — Bí quyết trang web "nhanh tức thì"

Đây là directive yêu thích của mình. Mình dùng nó cho hầu hết HTML và API responses, và kết quả luôn ấn tượng. Cơ chế hoạt động khá đơn giản:

  1. Fresh window (trong max-age): Trình duyệt dùng cache mà không hỏi server — nhanh nhất có thể
  2. Stale-while-revalidate window: Cache đã hết hạn, nhưng trình duyệt vẫn trả cache cũ cho người dùng ngay lập tức, đồng thời fetch phiên bản mới ở background. Lần truy cập sau sẽ nhận được phiên bản mới
  3. Expired window: Quá cả hai cửa sổ — trình duyệt bắt buộc phải chờ server response
# Ví dụ thực tế:
Cache-Control: max-age=600, stale-while-revalidate=30

# Timeline:
# 0-600 giây:   Fresh → trả cache ngay, không request
# 600-630 giây: Stale → trả cache ngay + revalidate ở background
# 630+ giây:    Expired → chờ server response

Lợi ích rõ ràng nhất: người dùng gần như luôn thấy trang tải tức thì. Trong hầu hết trường hợp, response được phục vụ từ cache. Nội dung vẫn được cập nhật — chỉ chậm hơn 1 lượt truy cập so với real-time. Đánh đổi hoàn toàn chấp nhận được.

2.5 ETag và conditional requests — Cơ chế revalidation

Khi cache hết hạn, trình duyệt không nhất thiết phải tải lại toàn bộ tài nguyên. Thay vào đó, nó gửi conditional request — kiểu như hỏi server "file này có thay đổi gì không?":

# Response ban đầu từ server:
HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "a1b2c3d4e5"
Content-Length: 45000

# Sau 1 giờ, browser gửi conditional request:
GET /style.css HTTP/1.1
If-None-Match: "a1b2c3d4e5"

# Nếu file không thay đổi:
HTTP/1.1 304 Not Modified
# → Không có body! Tiết kiệm 45KB bandwidth

# Nếu file đã thay đổi:
HTTP/1.1 200 OK
ETag: "f6g7h8i9j0"
Content-Length: 47000
# → Trả file mới

Mẹo nhỏ nhưng quan trọng: Nên dùng ETag thay vì Last-Modified cho revalidation. Last-Modified dựa trên timestamp, dễ gây lỗi khi deploy trên nhiều server (clock mismatch) hoặc khi file được rebuild với cùng nội dung. ETag dựa trên nội dung file nên chính xác hơn nhiều.

3. Service Worker Caching — Kiểm soát hoàn toàn với Workbox

3.1 Tại sao cần Service Worker khi đã có HTTP Cache?

HTTP cache tuyệt vời, nhưng nó có giới hạn khá rõ:

  • Không có offline support: HTTP cache chỉ hoạt động khi có kết nối mạng
  • Binary choice: Tài nguyên hoặc được cache, hoặc không — không có logic tùy chỉnh kiểu "nếu thế này thì thế kia"
  • Không kiểm soát được eviction: Browser tự quyết định khi nào xóa cache dựa trên dung lượng
  • Không precache được: Bạn không thể chủ động cache tài nguyên trước khi người dùng cần

Service Worker giải quyết tất cả những hạn chế này. Nó hoạt động như một proxy giữa trình duyệt và network, cho phép bạn viết logic caching tùy chỉnh theo ý muốn. Và Workbox — thư viện của Google, được sử dụng bởi 54% mobile sites — làm cho mọi thứ đơn giản hơn rất nhiều.

3.2 Cài đặt Workbox trong dự án Vite

# Cài đặt vite-plugin-pwa (tích hợp Workbox)
npm install -D vite-plugin-pwa
// vite.config.ts
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      strategies: 'injectManifest',
      srcDir: 'src',
      filename: 'sw.ts',
      injectRegister: 'auto',
      injectManifest: {
        globPatterns: ['**/*.{js,css,html,woff2,png,svg}'],
      },
    }),
  ],
});

3.3 Viết Service Worker với các chiến lược Workbox

Đây là file Service Worker hoàn chỉnh mà mình đang dùng trong production. Mỗi loại tài nguyên có chiến lược caching riêng — mình sẽ giải thích lý do bên dưới:

// src/sw.ts — Service Worker với Workbox strategies
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import {
  CacheFirst,
  StaleWhileRevalidate,
  NetworkFirst,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// 1. Precache — Cache tất cả static assets khi install
// Workbox tự generate manifest từ build output
precacheAndRoute(self.__WB_MANIFEST);

// 2. Cache First — Cho hình ảnh
// Lý do: Hình ảnh ít thay đổi, ưu tiên tốc độ
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({
        maxEntries: 100,      // Tối đa 100 hình
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 ngày
      }),
    ],
  })
);

// 3. Stale While Revalidate — Cho CSS và JS
// Lý do: Cân bằng giữa tốc độ và cập nhật
registerRoute(
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'static-resources',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 7 * 24 * 60 * 60, // 7 ngày
      }),
    ],
  })
);

// 4. Network First — Cho API responses
// Lý do: Dữ liệu cần fresh, nhưng có fallback khi offline
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3, // Timeout 3s rồi dùng cache
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 24 * 60 * 60, // 1 ngày
      }),
    ],
  })
);

// 5. Cache First — Cho Google Fonts
registerRoute(
  ({ url }) =>
    url.origin === 'https://fonts.googleapis.com' ||
    url.origin === 'https://fonts.gstatic.com',
  new CacheFirst({
    cacheName: 'google-fonts',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({
        maxEntries: 30,
        maxAgeSeconds: 365 * 24 * 60 * 60, // 1 năm
      }),
    ],
  })
);

3.4 Precaching — Tải trước tài nguyên quan trọng

Precaching là một trong những tính năng "game-changer" của Service Worker. Thay vì chờ người dùng truy cập từng trang rồi mới cache, bạn cache sẵn tài nguyên quan trọng ngay khi Service Worker được cài đặt. Mình từng thấy trang load time giảm đáng kể chỉ nhờ precaching đúng cách.

// Workbox tự động generate precache manifest từ build output
// Ví dụ manifest được generate:
[
  { url: '/index.html', revision: 'abc123' },
  { url: '/assets/app-Bk2x9f.js', revision: null },
  { url: '/assets/style-Dw4m1a.css', revision: null },
  { url: '/offline.html', revision: 'def456' },
]

// File có hash trong tên (app-Bk2x9f.js) không cần revision
// vì URL thay đổi khi nội dung thay đổi
// File không có hash (index.html) cần revision để Workbox
// biết khi nào cần update cache

Khi Workbox phát hiện revision thay đổi, nó tự động fetch phiên bản mới và cập nhật cache. Người dùng nhận được phiên bản mới ở lần truy cập tiếp theo — mượt mà, không cần đợi.

3.5 Offline fallback — Trải nghiệm không mạng

// Thêm vào sw.ts — Offline fallback page
import { setCatchHandler } from 'workbox-routing';

// Khi tất cả các strategy đều fail (offline + không có cache)
setCatchHandler(async ({ event }) => {
  if (event.request.destination === 'document') {
    // Trả về trang offline đã precache
    return caches.match('/offline.html');
  }
  // Cho các request khác, trả Response rỗng
  return Response.error();
});

3.6 Lưu ý quan trọng: Service Worker và HTTP Cache chồng lên nhau

Đây là điểm mà mình thấy rất nhiều developer mắc sai lầm — kể cả những người có kinh nghiệm. Service Worker cache và HTTP cache là hai tầng riêng biệt, và chúng có thể xung đột theo cách khó debug:

  • Nếu HTTP cache set max-age=86400 và Service Worker dùng NetworkFirst, Service Worker sẽ gửi request ra network — nhưng HTTP cache trả cached response trước khi request đến server. Kết quả? Service Worker nghĩ đó là response mới từ server, nhưng thực ra là bản cũ từ HTTP cache. Khá là "tricky" phải không?
  • Giải pháp: Với tài nguyên mà Service Worker quản lý, set Cache-Control: max-age=0 ở HTTP level. Để Service Worker lo toàn bộ caching logic
# Nginx: Tài nguyên được Service Worker quản lý
location /api/ {
    # Disable HTTP cache — để Service Worker xử lý
    add_header Cache-Control "max-age=0";
}

location ~* \.(html)$ {
    # HTML cũng nên để Service Worker quản lý
    add_header Cache-Control "no-cache";
}

4. CDN Edge Caching — Đưa nội dung gần người dùng hơn

4.1 CDN caching hoạt động như thế nào?

CDN (Content Delivery Network) đặt các edge server trên khắp thế giới. Ví dụ thực tế: khi người dùng ở Hà Nội truy cập trang web có origin server ở Singapore, CDN edge server ở Hà Nội sẽ phục vụ nội dung thay vì phải round-trip 30-50ms đến Singapore. Nghe đơn giản, nhưng hiệu quả thì không đơn giản chút nào.

CDN sử dụng header Cache-Control — đặc biệt là s-maxage — để quyết định cache bao lâu. Mỗi CDN provider (Cloudflare, AWS CloudFront, Google Cloud CDN) lại có thêm cấu hình riêng nữa.

4.2 Cấu hình Cloudflare Cache Rules

# Cloudflare Page Rules / Cache Rules
# Ví dụ: Cache HTML ở edge 5 phút, stale-while-revalidate 1 phút

# Cấu hình Cache-Control từ origin:
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60

# max-age=60      → Browser cache 1 phút
# s-maxage=300    → Cloudflare edge cache 5 phút
# stale-while-revalidate=60 → Cho phép stale thêm 1 phút

# Kết quả:
# - Người dùng đầu tiên: Request → Cloudflare edge → Origin → ~200ms
# - Người dùng thứ hai (trong 5 phút): Request → Cloudflare edge → ~20ms
# - Tiết kiệm: 90% thời gian response!

4.3 Cache HTML ở CDN — Kỹ thuật giảm TTFB mạnh nhất

Cache tài nguyên tĩnh (JS, CSS, hình ảnh) ở CDN là chuyện bình thường — ai cũng làm. Nhưng cache HTML ở CDN? Đây là bước nâng cấp mà ít team thực hiện, nhưng lại mang lại hiệu quả TTFB lớn nhất mà mình từng thấy.

Thách thức chính: HTML thường chứa nội dung dynamic (tên user, giỏ hàng, v.v.). Vậy giải pháp là gì?

  • Static HTML + Client-side hydration: Cache shell HTML ở CDN, load dynamic content bằng JavaScript. Phù hợp cho SPA
  • Edge-side Includes (ESI): CDN compose HTML từ nhiều fragment — cache phần static, fetch phần dynamic. Cloudflare Workers hỗ trợ pattern này khá tốt
  • stale-while-revalidate: Cache HTML với TTL ngắn, dùng SWR để luôn phục vụ nhanh
// Ví dụ: Cloudflare Worker cho HTML edge caching
// với stale-while-revalidate pattern

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Chỉ cache GET requests cho HTML
    if (request.method !== 'GET') {
      return fetch(request);
    }

    // Tạo cache key
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;

    // Kiểm tra edge cache
    let response = await cache.match(cacheKey);

    if (response) {
      // Cache HIT — trả ngay cho user
      // Đồng thời revalidate ở background
      const age = response.headers.get('age') || '0';
      if (parseInt(age) > 60) {
        // Cache cũ hơn 60s → revalidate
        // waitUntil cho phép chạy background task
        // mà không chặn response
        const ctx = request;
        event.waitUntil(
          fetch(request).then((freshResponse) => {
            if (freshResponse.ok) {
              cache.put(cacheKey, freshResponse.clone());
            }
          })
        );
      }
      return response;
    }

    // Cache MISS — fetch từ origin
    response = await fetch(request);

    if (response.ok) {
      const newResponse = new Response(response.body, response);
      newResponse.headers.set(
        'Cache-Control',
        'public, max-age=300, s-maxage=300'
      );
      // Lưu vào edge cache
      event.waitUntil(cache.put(cacheKey, newResponse.clone()));
      return newResponse;
    }

    return response;
  },
};

4.4 Cache invalidation — "Bài toán khó nhất trong computer science"

Phil Karlton từng nói: "Chỉ có hai điều khó trong computer science: cache invalidation và đặt tên." Câu này cũ rồi nhưng vẫn đúng. Khi bạn cache HTML ở CDN, bạn cần cách purge cache khi nội dung thay đổi:

  • Fingerprinted assets: Không cần invalidation — URL mới tự động tạo cache entry mới
  • HTML và API: Dùng API purge của CDN provider. Cloudflare, CloudFront đều có purge API khá tiện
  • Surrogate keys / cache tags: Gắn tag cho mỗi response (ví dụ: product-123), purge theo tag khi product thay đổi — hiệu quả hơn purge toàn bộ rất nhiều
# Ví dụ: Purge Cloudflare cache bằng API
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://example.com/blog/post-1"]}'

5. Cache Partitioning — Thay đổi quan trọng trong Chrome 2026

5.1 HTTP Cache Partitioning là gì?

Từ Chrome 86, Google đã triển khai HTTP cache partitioning — chia cache theo top-level site thay vì chỉ theo URL. Nghe có vẻ kỹ thuật, nhưng ý nghĩa thực tế rất rõ ràng:

  • Trước đây: Nếu site-a.comsite-b.com cùng load jQuery từ cdn.jsdelivr.net, trình duyệt chỉ tải 1 lần
  • Bây giờ: Mỗi top-level site có cache riêng — jQuery được tải 2 lần, mỗi lần cho mỗi site

Lý do? Bảo mật. Cache chia sẻ có thể bị khai thác để tracking người dùng qua các trang web (side-channel attacks). Cache partitioning loại bỏ rủi ro này — đánh đổi hiệu năng nhỏ để được bảo mật tốt hơn.

5.2 Tác động đến performance và cách thích ứng

Theo dữ liệu từ Chrome, cache partitioning tăng tỷ lệ cache miss khoảng 3.6% và tăng bytes tải từ network khoảng 4%. FCP tăng nhẹ ~0.3%. Nghe thì không nhiều, nhưng nó thay đổi cách chúng ta nghĩ về CDN strategy.

Cụ thể hơn:

  • Self-hosting tốt hơn CDN chia sẻ: Vì cache không còn được share giữa các site, lợi ích "cache hit từ site khác" của public CDN (như cdn.jsdelivr.net) gần như biến mất. Self-hosting (hoặc dùng CDN riêng) giờ là lựa chọn hợp lý hơn
  • Bundle size quan trọng hơn bao giờ hết: Mỗi user phải tải tất cả từ đầu cho mỗi site. Code splitting và tree shaking không còn là "nice-to-have" — nó là bắt buộc
  • Service Worker precaching giá trị hơn: Service Worker cache không bị partitioning theo cùng cách, và bạn kiểm soát hoàn toàn lifecycle của cache

6. Kết hợp các tầng caching — Chiến lược tổng thể

6.1 Ma trận chiến lược caching theo loại tài nguyên

Sau khi đi qua từng tầng, đây là bảng tổng hợp mà mình khuyến nghị cho năm 2026. Mình dùng bảng này như một "cheat sheet" cho mọi dự án:

┌─────────────────────┬──────────────────────────────┬─────────────────────┬──────────────────┐
│ Loại tài nguyên     │ HTTP Cache-Control           │ Service Worker      │ CDN Edge         │
├─────────────────────┼──────────────────────────────┼─────────────────────┼──────────────────┤
│ JS/CSS (hashed)     │ max-age=1y, immutable        │ Precache            │ max-age=1y       │
│ Hình ảnh            │ max-age=1d, swr=1h           │ CacheFirst, 30d    │ max-age=1d       │
│ Font (woff2)        │ max-age=1y, immutable        │ CacheFirst, 1y     │ max-age=1y       │
│ HTML                │ max-age=1m, s-maxage=5m      │ NetworkFirst, 3s   │ s-maxage=5m      │
│ API (public)        │ max-age=0, s-maxage=1m       │ NetworkFirst, 3s   │ s-maxage=1m      │
│ API (private)       │ private, no-store            │ NetworkFirst       │ Không cache      │
│ Media (video/audio) │ max-age=7d, swr=1d           │ Không cache (lớn)  │ max-age=7d       │
└─────────────────────┴──────────────────────────────┴─────────────────────┴──────────────────┘

swr = stale-while-revalidate
1y = 1 year (31536000s), 1d = 1 day (86400s), 1m = 1 minute (60s)

6.2 Checklist caching performance audit

Mình tạo checklist này để tự kiểm tra mỗi khi setup caching cho dự án mới. Bạn cũng có thể dùng nó như một bước cuối trước khi deploy:

  1. Static assets có fingerprint không? Tên file phải chứa content hash (ví dụ: app-Bk2x9f.js)
  2. Fingerprinted assets có immutable không? Cache-Control: public, max-age=31536000, immutable
  3. HTML có stale-while-revalidate không? Giúp returning visitors thấy trang tải tức thì
  4. CDN có cache HTML không? Dùng s-maxage để cache ở edge mà không ảnh hưởng browser cache
  5. Service Worker có chiến lược phù hợp không? CacheFirst cho static, NetworkFirst cho dynamic
  6. Service Worker và HTTP cache có xung đột không? Kiểm tra tài nguyên dùng NetworkFirst có max-age=0 ở HTTP level
  7. ETag có được thiết lập không? Cho phép revalidation hiệu quả khi cache hết hạn
  8. Không có no-store trên static assets? no-store chỉ nên dùng cho dữ liệu nhạy cảm thôi

7. Đo lường hiệu quả caching

7.1 Chrome DevTools — Network tab

Cách đơn giản nhất để kiểm tra caching — và cũng là bước đầu tiên mình luôn làm:

  1. Mở DevTools → Network tab
  2. Tải trang lần đầu, ghi nhận các request
  3. Tải lại trang (F5), quan sát cột Size:
    • "(disk cache)": Tài nguyên phục vụ từ HTTP cache trên đĩa — tốt
    • "(memory cache)": Phục vụ từ bộ nhớ — nhanh hơn disk, rất tốt
    • "(ServiceWorker)": Phục vụ từ Service Worker cache — tuyệt vời
    • Số KB: Tài nguyên phải tải từ network — cần xem lại caching config

7.2 Lighthouse và cache audit

Lighthouse có audit "Serve static assets with an efficient cache policy" — nó liệt kê tất cả tài nguyên có TTL ngắn hoặc thiếu Cache-Control header. Đây là cách nhanh nhất để phát hiện tài nguyên chưa được cache hợp lý. Nếu bạn chỉ có 5 phút để audit caching, hãy chạy cái này.

7.3 Đo cache hit rate với Server-Timing header

# Thêm Server-Timing header để đo performance
# Nginx example:
add_header Server-Timing "cache;desc=\"HIT\";dur=2" always;

# CDN (Cloudflare) tự thêm header:
# cf-cache-status: HIT  → Phục vụ từ edge cache
# cf-cache-status: MISS → Phải fetch từ origin
# cf-cache-status: EXPIRED → Cache hết hạn, revalidating

FAQ — Câu hỏi thường gặp về Caching Web Performance

Cache-Control no-cache và no-store khác nhau như thế nào?

no-cache vẫn lưu tài nguyên vào cache, nhưng bắt buộc phải revalidate với server mỗi lần sử dụng. no-store hoàn toàn không lưu — tài nguyên phải tải mới hoàn toàn mỗi lần. Dùng no-cache cho HTML cần luôn mới nhất (kết hợp ETag), dùng no-store cho dữ liệu nhạy cảm (thông tin tài khoản, token). Đây là lỗi nhầm lẫn phổ biến nhất mà mình gặp khi review code.

Service Worker cache có bị xóa khi người dùng xóa browser cache không?

Có. Khi người dùng xóa "Cached images and files" hoặc "All site data" trong trình duyệt, Service Worker cache (Cache Storage) cũng bị xóa. Tuy nhiên, Service Worker script vẫn được đăng ký — lần truy cập tiếp theo, nó sẽ tự động precache lại. Đây cũng là lý do mình luôn khuyên kết hợp cả hai tầng cache thay vì chỉ dựa vào một.

stale-while-revalidate có ảnh hưởng đến SEO không?

Không ảnh hưởng tiêu cực. Googlebot đã hỗ trợ stale-while-revalidate và hiểu rằng nội dung có thể hơi cũ trong thời gian revalidation. Thực tế, SWR còn giúp cải thiện Core Web Vitals (nhất là LCP và TTFB), gián tiếp tốt cho SEO. Chỉ cần giữ max-age và SWR window ở mức hợp lý — vài phút đến vài giờ cho nội dung thay đổi thường xuyên.

Nên dùng CDN chia sẻ (như cdnjs, jsdelivr) hay self-hosting trong năm 2026?

Self-hosting là lựa chọn tốt hơn trong năm 2026, không còn tranh cãi nhiều nữa. Với HTTP cache partitioning, cache từ CDN chia sẻ không còn được share giữa các website. Self-hosting qua CDN riêng cho phép kiểm soát hoàn toàn Cache-Control headers, tránh phụ thuộc bên thứ ba, và tận dụng HTTP/2 multiplexing tốt hơn vì tất cả tài nguyên cùng origin.

Làm sao biết chiến lược caching đang hoạt động hiệu quả?

Theo dõi ba chỉ số chính: (1) Cache hit rate ở CDN — target trên 90% cho static assets. (2) Số lượng request "disk cache" và "memory cache" trong Chrome DevTools khi reload trang — càng nhiều càng tốt. (3) TTFB của returning visitors so với first-time visitors — nếu returning nhanh hơn đáng kể, chiến lược caching của bạn đang hoạt động đúng.

Về Tác Giả Editorial Team

Our team of expert writers and editors.