HTTP caching headers: hiểu đúng để cache hoạt động

5 phút đọc

Read in English →

Lỗi caching có một đặc điểm riêng: mọi thứ chạy tốt trên máy của bạn, rồi một khách hàng báo rằng giá của ngày hôm qua vẫn đang hiển thị. Phần lớn sự nhầm lẫn bắt nguồn từ một số ít directive trông giống nhau nhưng hoạt động khác nhau. Dưới đây là bản đồ thực tế về việc mỗi directive làm gì, nên dùng tổ hợp nào cho loại tệp nào, và cách tìm hiểu CDN của bạn đang thực sự làm gì.

Hai câu hỏi mà mọi cache đều đặt ra

Trước tiên cache cần biết bản đã lưu còn mới (fresh) hay không, và thứ hai, nếu đã cũ (stale) thì liệu có gì thực sự thay đổi không. Độ mới được điều khiển bởi các directive về thời hạn như max-age. Việc phát hiện thay đổi do validator đảm nhiệm: một giá trị ETag hoặc Last-Modified mà client gửi lại trong một conditional request. Nếu không có gì thay đổi, server trả lời 304 Not Modified không kèm body, và bản đã lưu lại trở thành mới.

Những directive hay bị nhầm lẫn

  • no-cache không có nghĩa là “không cache”. Response vẫn có thể được lưu, nhưng phải được xác thực lại với origin mỗi lần trước khi dùng lại.
  • no-store mới là directive ngăn việc lưu trữ. MDN lưu ý đây không phải lúc nào cũng là lựa chọn tốt nhất, vì nó cũng làm mất các lợi ích như back/forward cache.
  • private giữ response ra khỏi các shared cache như CDN. Chỉ riêng một cookie không khiến response trở thành private.
  • s-maxage chỉ áp dụng cho shared cache và ghi đè max-age ở đó, nên trình duyệt và CDN có thể giữ các thời hạn khác nhau.
  • immutable cam kết tệp sẽ không bao giờ thay đổi tại URL đó, điều này chỉ an toàn khi URL chứa phiên bản hoặc hash.

Hãy tránh phụ thuộc vào heuristic caching. Khi không có header tường minh, cache vẫn có thể lưu một response có ngày Last-Modified và dùng lại trong một phần của tuổi đời biểu kiến của nó, khiến hành vi khó đoán.

Một bộ header cho từng loại tệp

Hãy cho mỗi nhóm tài nguyên một chính sách riêng:

  • Asset tĩnh có hash (app.3f9c1a.js, font, ảnh có content hash): public, max-age=31536000, immutable. Đổi tệp thì đổi tên, nên mục cũ đơn giản là không bao giờ được yêu cầu nữa.
  • Trang HTML: no-cache, để mỗi lượt truy cập đều xác thực lại và nhận tên asset mới. Thêm private nếu trang được cá nhân hóa.
  • Dữ liệu API hoặc danh mục hơi cũ cũng chấp nhận được: max-age ngắn kèm stale-while-revalidate.
  • Mọi thứ liên quan đến đăng nhập, session hoặc thanh toán: no-store.
location ~* \.(?:js|css|woff2|avif|webp|png|jpg)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

location = / {
  add_header Cache-Control "no-cache";
}

location /api/products {
  add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
}

Khối đầu tiên giả định công cụ build của bạn gắn fingerprint vào tên tệp. Nếu không, đừng dùng thời hạn một năm; cache không thể biết khi nào tệp thay đổi.

stale-while-revalidate làm gì

Directive này cho phép cache trao ngay một bản đã hết hạn trong khi nó lấy bản mới ở chế độ nền. Phần giải thích của DebugBear dùng ba trạng thái: fresh, stale và quá cũ để phục vụ. Giá trị max-age đánh dấu điểm kết thúc của trạng thái fresh, còn giá trị stale-while-revalidate kéo dài thêm khoảng thời gian sau đó mà bản stale vẫn được phép phục vụ. Với max-age=60, stale-while-revalidate=300, các request trong 60 giây đầu được phục vụ thẳng từ cache, các request trong khoảng từ 60 đến 360 giây nhận bản stale kèm một lần làm mới ở nền, và các request sau đó phải chờ origin.

Nó phù hợp với nội dung được phép chậm hơn một chút, như danh mục sản phẩm, tài liệu và các trang blog. Nó không phù hợp với mức tồn kho, đặt chỗ, trạng thái đăng nhập hay bất cứ thứ gì phải luôn cập nhật. Một directive đi kèm, stale-if-error, phục vụ bản đã lưu khi origin đang gặp lỗi.

Vary và tỷ lệ cache hit

Mặc định cache dùng URL làm khóa. Thêm một tên header vào Vary sẽ tách mục cache theo từng giá trị của header đó. Vary: Accept-Language giữ trang tiếng Anh và tiếng Nhật tách biệt, đúng như bạn muốn. Vary: User-Agent tạo ra nhiều biến thể đến mức shared caching gần như vô hiệu, nên hãy dùng feature detection thay thế. Với nội dung cá nhân hóa, private thường là câu trả lời tốt hơn việc vary theo cookie.

Debug một CDN liên tục bị miss

Hãy so sánh những gì origin gửi với những gì edge trả về, vì một số CDN thêm hoặc xóa header:

curl -sI https://origin.example.com/styles.css | grep -i -E 'cache-control|etag|age'
curl -sI https://www.example.com/styles.css    | grep -i -E 'cache-control|etag|age|cf-cache-status|x-cache'

Các chỉ báo riêng của từng nhà cung cấp gồm CF-Cache-Status trên Cloudflare và X-Cache trên Fastly và CloudFront, còn header Age cho biết một response đã được lưu bao lâu. Một response miss ở mọi lần thường có Cache-Control: private, header Set-Cookie hoặc header Authorization đang tác động.

Thêm một điều cần lưu ý: purge CDN không xóa cache trong trình duyệt của ai cả. Khách truy cập giữ bản của họ cho đến khi max-age hết, đó là lý do tên tệp có hash tốt hơn việc purge quyết liệt.

Nếu storefront hoặc site WordPress của bạn cần một đợt rà soát caching, dịch vụ của chúng tôi bao gồm thiết lập CDN và audit hiệu năng.

Đọc thêm

← Tất cả bài viết