Cache là một trong những cách hiệu quả nhất để tăng hiệu năng cho web. Nhưng dùng sai thì nó gây ra rất nhiều bug khó chịu: dữ liệu cũ, UI không cập nhật, hoặc người dùng thấy nội dung khác nhau.
§1Cache-Control là gì?
Cache-Control là HTTP header chỉ định cách một response được lưu cache và khi nào được phép dùng lại. Server gửi header này cho client (browser, CDN, proxy).
Ví dụ:
Cache-Control: public, max-age=0, must-revalidate§2Các directive quan trọng
2.1 public vs private
publiccho phép mọi cache lưu lại, gồm browser, CDN và proxy.privatechỉ cho cache ở browser của user, không cache ở CDN.
Quy tắc chung: static assets dùng public, data cá nhân dùng private.
2.2 max-age
Cache-Control: max-age=3600Response được coi là fresh trong 3600 giây (1 giờ). Trong thời gian này không cần gọi server, cache được dùng trực tiếp. Đây là cách tối ưu performance mạnh nhất.
2.3 no-cache
Cache-Control: no-cacheCái tên dễ làm hiểu nhầm thành "không cache". Thực ra response vẫn được cache, chỉ là phải kiểm tra với server trước khi dùng.
2.4 no-store
Cache-Control: no-storeKhông lưu cache ở bất kỳ đâu. Dùng cho thông tin nhạy cảm như banking, auth.
2.5 must-revalidate
Cache-Control: must-revalidateKhi cache hết hạn, bắt buộc phải hỏi lại server, không được dùng cache cũ.
2.6 s-maxage và stale-while-revalidate
Khi có CDN đứng giữa, hai directive này rất hay dùng:
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=30s-maxagelà thời gian fresh riêng cho cache dùng chung (CDN, proxy), ghi đèmax-ageở đó.stale-while-revalidatecho phép trả bản cũ thêm 30 giây sau khi hết hạn, trong lúc cache lấy bản mới ở background.
Với ví dụ trên, browser giữ 60 giây, CDN giữ 10 phút, và người dùng không phải chờ khi CDN làm mới.
§3Hiểu sâu qua ví dụ thực tế
Cache-Control: public, max-age=0, must-revalidateÝ nghĩa:
public: cache được lưu ở mọi nơimax-age=0: hết hạn ngay lập tứcmust-revalidate: phải check server trước khi dùng
Gộp lại: response có thể cache, nhưng mỗi lần dùng phải hỏi server xem có bản mới không.
§4Cơ chế revalidation
Khi cache cần kiểm tra lại, nó dùng ETag hoặc Last-Modified.
ETag
Server trả:
ETag: "abc123"Client gửi lại:
If-None-Match: "abc123"Nếu không thay đổi:
HTTP 304 Not ModifiedServer không cần gửi lại body, nên tiết kiệm được bandwidth.
Last-Modified
Tương tự:
Last-Modified: Thu, 26 Mar 2026 10:00:00 GMTClient:
If-Modified-Since: ...§5Các chiến lược cache phổ biến
5.1 Static assets (JS, CSS, images)
Cache-Control: public, max-age=31536000, immutableCache 1 năm và không bao giờ revalidate, dùng kèm filename hash (app.abc123.js). Đây là cấu hình cho performance tốt nhất.
5.2 HTML (dynamic content)
Cache-Control: public, max-age=0, must-revalidateLuôn check server, nhưng vẫn tận dụng được 304. Phù hợp với SSR và content thay đổi thường xuyên.
Nếu HTML chứa dữ liệu của từng user (dashboard, trang tài khoản) thì không dùng public: CDN có thể lưu trang của người này và trả cho người khác. Dùng:
Cache-Control: private, no-cache5.3 API responses
Trường hợp real-time
Cache-Control: no-storeTrường hợp có thể cache nhẹ
Cache-Control: public, max-age=60Ví dụ list sản phẩm, hoặc data không thay đổi liên tục.
5.4 User-specific data
Cache-Control: private, max-age=300Cache ở browser và không leak qua CDN.
§6Những sai lầm phổ biến
1. Dùng no-cache khi muốn "không cache"
no-cache vẫn cache. Muốn không cache thì phải dùng no-store.
2. Không version static assets
Người dùng bị kẹt với file cũ. Luôn đặt hash vào tên file:
app.[hash].js3. Cache API mà không nghĩ về consistency
User thấy dữ liệu cũ, và bug kiểu này rất khó debug.
4. Không hiểu CDN cache
CDN có thể cache ngay cả khi bạn không để ý, nên cần đặt rõ public hay private.
5. Dùng public cho trang có dữ liệu user
CDN trả dashboard của user này cho user khác. Trang nào phụ thuộc vào người đang đăng nhập thì dùng private.
§7Best Practices (tóm tắt)
Static assets
public, max-age=31536000, immutableHTML
public, max-age=0, must-revalidateAPI
- Real-time:
no-store - Semi-static:
max-age=60
Sensitive data
no-storeLuôn dùng versioning
Hash filename để tránh cache bug.
§8Ví dụ: site tĩnh trên Cloudflare Pages
Phần trên khá lý thuyết, nên mình thêm một ví dụ cụ thể: một site Next.js export tĩnh deploy lên Cloudflare Pages. Cloudflare Pages đọc header tuỳ chỉnh từ file _headers đặt trong thư mục output (với Next.js là public/_headers, khi build sẽ được copy ra output).
Next.js đặt mọi JS và CSS đã build trong /_next/static/, và tên file có hash. Nội dung đổi thì tên file đổi, nên các file này cache được rất lâu:
# public/_headers
/_next/static/*
Cache-Control: public, max-age=31536000, immutableHTML thì ngược lại: tên file không đổi (/index.html, /posts/abc/index.html), nên phải luôn kiểm tra lại với server. Mình không thêm rule /* cho HTML vào file này. Cloudflare Pages gộp header của mọi rule khớp với một URL, và nếu hai rule cùng đặt Cache-Control thì hai giá trị bị nối bằng dấu phẩy. Một file trong /_next/static/ khớp cả /* lẫn /_next/static/* sẽ nhận header kiểu public, max-age=0, must-revalidate, public, max-age=31536000, immutable.
Nếu thật sự cần đặt header cho mọi đường dẫn, dùng cú pháp ! để gỡ header của rule rộng hơn trước khi đặt lại:
/*
Cache-Control: public, max-age=0, must-revalidate
/_next/static/*
! Cache-Control
Cache-Control: public, max-age=31536000, immutableSau khi deploy, kiểm tra header thực tế bằng curl:
curl -sI https://example.com/ | grep -i cache-control
curl -sI https://example.com/_next/static/chunks/main-abc123.js | grep -i cache-controlThay example.com bằng domain của bạn và đường dẫn file JS bằng một file thật lấy từ tab Network của trình duyệt.
§9Kết
Cache-Control nhìn thì đơn giản nhưng ảnh hưởng rất lớn đến hệ thống. Đặt đúng thì giảm load cho server và app chạy nhanh hơn, đồng thời tránh được hàng loạt bug khó chịu kể trên.