Web Performance·

HTTP Caching: Tại Sao Browser Của Bạn Thông Minh Hơn Bạn Nghĩ

Hướng dẫn thực tế về Cache-Control, ETag, và request lifecycle — cụ thể nên set gì, tại sao, và những lỗi hay gặp nhất.
Deep dive: web-performance →

Browser vừa tải xong trang này. Nếu bạn refresh ngay bây giờ, hầu hết các file sẽ không được download lại — chúng được serve từ disk hoặc memory trong vài millisecond. Đó là HTTP caching. Hiểu cách nó quyết định lưu gì, giữ bao lâu, và khi nào hỏi lại server — là một trong những kỹ năng highest-leverage trong web performance.

Post này cover hai cơ chế chính: Cache-ControlETag/Last-Modified, khi nào dùng cái nào, và những mistake hay gặp nhất.

Hai Cơ Chế, Một Mục Tiêu

HTTP caching hoạt động theo hai hướng khác nhau:

Cơ chếCâu hỏi nó trả lờiCó round trip không?
Cache-Control"Tôi có cần hỏi server không?"Không — serve thẳng từ cache
ETag / Last-Modified"Nội dung có thay đổi kể từ lần cuối fetch?"Có — nhưng response body có thể rỗng (304)

Phân biệt được hai cái này là chìa khóa để dùng caching đúng.

Cache-Control: Lever Chính

Cache-Control là response header nói cho browser (và CDN nếu có) biết cách xử lý bản cache hiện tại.

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

Response Headers
Cache-Control: max-age=31536000, immutable
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: max-age=0, must-revalidate
immutable nói với browser "URL này sẽ không bao giờ thay đổi nội dung." Kết hợp với content hash trong tên file (main.a3f92b.js), bạn có thể cache vô hạn mà không lo stale asset.

Ý nghĩa thực tế của từng directive:

max-age=N

Cache response này trong N giây. Trong khoảng thời gian đó, browser serve thẳng mà không hỏi server. Với static asset có hash trong tên file, set 31536000 (1 năm) là an toàn.

no-cache

Dù tên là vậy, directive này không tắt caching. Nó có nghĩa là "luôn revalidate trước khi serve." Browser vẫn cache response nhưng check với server mỗi request. Đây là lựa chọn đúng cho HTML page.

no-store

Cái này mới thực sự tắt caching. Không lưu gì cả. Dùng cho sensitive data (user dashboard, banking data) — không phải default cho mọi route.

must-revalidate

Khi max-age hết hạn, browser phải liên hệ server trước khi serve bản cũ. Tránh serve nội dung lỗi thời ngay cả khi network chập chờn.

ETag: Conditional Request

Khi max-age hết hạn (hoặc bạn set no-cache), browser gửi conditional request để check xem có gì thay đổi không:

sequenceDiagram
    Browser->>Server: GET /api/data<br/>If-None-Match: "abc123"
    alt Nội dung không đổi
        Server-->>Browser: 304 Not Modified<br/>(không có body)
    else Nội dung đã thay đổi
        Server-->>Browser: 200 OK<br/>ETag: "xyz789"<br/>(full response body)
    end

ETag là hash của nội dung response. Nếu khớp với cái browser gửi lên, server trả về 304 Not Modified — chỉ có headers, không có body. Browser dùng lại bản cache đã có.

Last-Modified hoạt động tương tự nhưng dùng timestamp thay vì hash. ETag đáng tin hơn vì timestamp chỉ có độ chính xác 1 giây và có thể khác nhau giữa các server trong một cluster.

Chiến Lược Đúng Cho Từng Loại Asset

Không phải asset nào cũng cần cùng một cache policy. Đây là decision matrix:

Loại assetCache-Control nên dùngLý do
JS/CSS có hash trong tên filemax-age=31536000, immutableHash đổi = URL mới = safe mãi mãi
HTML pageno-cacheLuôn fresh, validate bằng ETag
API response (public)max-age=60, stale-while-revalidate=300Serve nhanh, refresh ngầm
API response (private)no-storeKhông cache — bao giờ hết
Font tự hostmax-age=31536000, immutableFont hiếm khi thay đổi
Ảnh từ CMSmax-age=86400 + ETagFreshness theo ngày + conditional

Pattern stale-while-revalidate

Một trong những directive hữu ích nhất nhưng hay bị hiểu sai:

Response Headers
Cache-Control: max-age=60, stale-while-revalidate=300

Cụ thể nó làm gì:

  • 60 giây đầu: serve thẳng từ cache, không có network
  • Từ 60 đến 360 giây: serve bản cũ ngay lập tức, đồng thời fetch bản mới ở background
  • Sau 360 giây: bắt buộc phải revalidate trước khi serve

Kết quả là UI không bao giờ block vì cache miss trong vòng 6 phút, nhưng vẫn đủ fresh.

stale-while-revalidate không phù hợp cho data mà stale = sai (ví dụ: số lượng tồn kho, auth state). Dùng cho nội dung mà việc trễ 5 phút là chấp nhận được.

Những Lỗi Hay Gặp

Cache-Control: no-cache
Cache-Control: no-store

Set no-cache trên file JS có hash là lỗi phổ biến nhất. File đó đã có cache-busting built-in trong tên file rồi — thêm no-cache chỉ tạo thêm round trip mỗi lần load trang mà không có lợi gì.

Key Takeaways

  • Cache-Control: max-age + immutable là lựa chọn đúng cho hashed static asset. Set một lần, không cần nghĩ lại.
  • no-cache có nghĩa là "luôn revalidate", không phải "không cache". Dùng cho HTML.
  • no-store chỉ dành cho sensitive data — không phải default cho mọi API route.
  • ETag / 304 Not Modified giảm một nửa bandwidth cho resource hay thay đổi.
  • stale-while-revalidate là pattern background-refresh — tốt cho public API data.

Deep Dive: Web Performance

Khám phá toàn bộ 37 concept trong Web Performance knowledge graph — từ critical rendering path đến Core Web Vitals.

MDN: HTTP Caching

Tài liệu tham khảo đầy đủ nhất cho Cache-Control directives và conditional requests.