· cloudflare / cloudflare-pages / cloudflare-workers
Cách cache static assets đúng cách trên Cloudflare
Cấu hình Cache-Control đúng cho fingerprinted assets, index.html và service workers trên Cloudflare. Xác minh bằng cf-cache-status; sáu lỗi BYPASS phổ biến.
Bởi Ethan
2.355 từ · 12 phút đọc
Nhiều deployment trên Cloudflare đang bỏ phí hiệu năng. Dấu hiệu rõ nhất là cf-cache-status: MISS xuất hiện trên mọi asset request. Không phải vì caching khó, mà vì Cloudflare edge không hoạt động như một CDN thông thường — nó có quy tắc riêng về những gì sẽ được cache, khi nào sẽ bị bypass, và cách nó tương tác với các header bạn thiết lập.
Bài viết này sẽ giải quyết vấn đề đó. Bạn sẽ nắm được cách thiết lập header đúng cho từng loại asset, cấu hình hoạt động cho cả Cloudflare Pages và Workers, cùng một checklist để xác nhận mọi thứ đang chạy đúng.
Bài viết này dành cho ai
Dành cho developers đang deploy static sites hoặc SPAs trên Cloudflare Pages hoặc Workers và nhận được MISS hoặc BYPASS trên những assets đáng lẽ phải được cache. Bài viết giả định bạn đã quen cơ bản với Cloudflare. Nếu bạn đang so sánh Cloudflare với các nền tảng khác, xem Cloudflare Workers vs Vercel Edge.
Edge caching của Cloudflare hoạt động như thế nào
Cloudflare cache tại edge — các Points of Presence (PoPs) phân tán theo địa lý, nằm giữa người dùng và origin server của bạn. Khi một request đến edge node và asset đã có trong cache, request đó không bao giờ chạm tới server của bạn. Đó chính là lợi ích.
Cloudflare cache gì theo mặc định: dựa trên phần mở rộng file, không phải MIME type. Các phần mở rộng như .css, .js, .png, .jpg, .woff2, và hơn 60 loại static khác được cache tự động. HTML, JSON, và XML thì không — Cloudflare xem chúng là dynamic trừ khi bạn khai báo tường minh.
Edge TTL mặc định khi không có header Cache-Control:
| HTTP status | Edge TTL |
|---|---|
| 200, 206, 301 | 120 phút |
| 302, 303 | 20 phút |
| 404, 410 | 3 phút |
Khi Cloudflare sẽ không cache, bất kể loại file:
Cache-Control: private,no-store,no-cache, hoặcmax-age=0có trong response- Response có header
Set-Cookie(sẽ đề cập trong phần lỗi thường gặp) - HTTP method không phải GET
Edge cache vs. browser cache: max-age ảnh hưởng cả hai. s-maxage chỉ ảnh hưởng edge — browser bỏ qua nó. Điều này quan trọng: dùng s-maxage=86400, max-age=0 để cache tích cực tại Cloudflare trong khi buộc browser luôn revalidate.
Mỗi response từ Cloudflare đều có header cf-cache-status cho bạn biết chính xác điều gì đã xảy ra. Đây là thứ bạn sẽ dùng để xác minh mọi thứ hoạt động đúng.
Cây quyết định: header đúng theo loại asset
Làm đúng phần này trước. Header sai ở đây là nguyên nhân gốc rễ của hầu hết các response BYPASS và DYNAMIC.
Fingerprinted assets (bundle.abc123.js, main.7f3e1a.css)
Các tool như Vite, Astro, và Next.js tạo filename có content hash theo mặc định. Hash thay đổi khi nội dung file thay đổi. Nghĩa là bạn có thể cache chúng mãi mãi — URL đó sẽ không bao giờ trả về nội dung cũ.
Cache-Control: public, max-age=31536000, immutable
public— có thể cache tại CDN edge và browsermax-age=31536000— 1 năm trong browserimmutable— báo cho browser không cần revalidate khi reload; loại bỏ conditional GET requests khi nhấn F5
Lưu ý: immutable không có tác dụng trên edge của Cloudflare. Behavior tại edge được điều khiển bởi max-age (hoặc s-maxage). Vẫn nên thêm vào — nó cải thiện hiệu năng phía browser.
index.html và các entry points (không có fingerprint)
Các file này phải luôn mới khi người dùng truy cập. Một index.html cũ trỏ đến các asset hash cũ sẽ làm hỏng ứng dụng.
Cache-Control: no-cache
no-cache không có nghĩa là “đừng cache.” Nó có nghĩa là “cache đi, nhưng phải revalidate trước khi serve.” Cloudflare sẽ lưu response, gửi conditional request đến origin, và trả về nội dung mới nếu có thay đổi. Đây là behavior đúng cho trường hợp này.
Nếu bạn muốn cho phép edge cache ngắn hạn:
Cache-Control: public, s-maxage=60, max-age=0, must-revalidate
Cấu hình này cache tại edge trong 60 giây (hữu ích để hấp thụ traffic spike khi deploy) đồng thời buộc browser luôn kiểm tra lại.
Service workers (/sw.js)
Không bao giờ cache service workers lâu dài. Một service worker cũ trỏ đến asset URLs cũ sẽ trả về ứng dụng bị hỏng cho đến khi người dùng tự xóa cache — hoặc lâu hơn nếu vòng lặp update chính nó cũng bị lỗi.
Cache-Control: no-store
no-store có nghĩa là không lưu gì cả. Không tại edge, không tại browser. Luôn fetch mới hoàn toàn.
API responses và dynamic content
Cache-Control: private, no-cache
private báo cho edge của Cloudflare không cache response — chỉ browser mới có thể cache. no-cache cho phép browser lưu nhưng yêu cầu revalidate. Kết hợp này bảo vệ dữ liệu riêng tư của người dùng trong khi vẫn cho phép tối ưu hóa conditional request ở phía browser.
Bước 1: Cấu hình Cloudflare Pages với _headers
File _headers là một file text thuần trong thư mục build output — cùng thư mục với index.html. Cloudflare Pages đọc nó khi deploy và áp dụng các quy tắc.
# Fingerprinted assets — cache mãi mãi
/assets/*
Cache-Control: public, max-age=31536000, immutable
# Fonts — cũng có fingerprint trong hầu hết các build setup
/fonts/*
Cache-Control: public, max-age=31536000, immutable
# HTML entry points — buộc revalidation
/
Cache-Control: no-cache
/*.html
Cache-Control: no-cache
# Service worker — không lưu gì cả
/sw.js
Cache-Control: no-store
Ba giới hạn cần biết:
- Tối đa 100 header rules mỗi file
_headers. - Giới hạn 2.000 ký tự mỗi dòng.
_headerskhông có tác dụng với responses từ Pages Functions. Nếu một URL được phục vụ bởi Function (tức là có file tương ứng trong/functions), file_headerssẽ bị bỏ qua cho route đó. Thay vào đó, hãy set headers trongResponsecủa Function.
Với build Vite hoặc Astro thông thường, fingerprinted assets sẽ nằm trong /assets/ và file _headers sẽ bao phủ tất cả. Kiểm tra thư mục build output — file phải nằm ở đó, không phải trong source root, để Cloudflare nhận ra.
Nếu bạn đang thiết lập Cloudflare Pages từ đầu, xem Cách deploy Astro lên Cloudflare Pages để biết toàn bộ cấu hình build bao gồm cách đặt file _headers đúng vị trí.
Bước 2: Cấu hình Cloudflare Workers với response headers
Nếu bạn đang serve assets từ Worker thay vì Pages, hãy set headers trực tiếp trên Response object.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const response = await fetch(request);
// Clone response để headers có thể chỉnh sửa
const newResponse = new Response(response.body, response);
if (url.pathname.startsWith('/assets/')) {
newResponse.headers.set(
'Cache-Control',
'public, max-age=31536000, immutable'
);
} else if (
url.pathname === '/' ||
url.pathname.endsWith('.html')
) {
newResponse.headers.set('Cache-Control', 'no-cache');
} else if (url.pathname === '/sw.js') {
newResponse.headers.set('Cache-Control', 'no-store');
}
return newResponse;
}
};
Workers Cache API (để kiểm soát theo chương trình)
Dùng Cache API khi bạn cần cache nội dung không có cache headers, hoặc khi muốn cache và serve ngay trong Worker mà không cần chuyển tiếp đến origin cho mỗi request.
const cache = caches.default;
const cacheKey = new Request(request.url, request);
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(request);
const newRes = new Response(response.body, response);
// s-maxage kiểm soát edge TTL; max-age kiểm soát browser TTL
newRes.headers.append('Cache-Control', 's-maxage=86400');
ctx.waitUntil(cache.put(cacheKey, newRes.clone()));
}
return response;
Một hạn chế quan trọng: cache.delete() chỉ xóa nội dung khỏi PoP local. Để purge toàn cầu, dùng Cloudflare Cache Purge API — Workers Cache API không đồng bộ purge trên các data center.
Tùy chọn cf của Workers
Một cách thay thế cho việc chỉnh headers. Thuộc tính cf trên fetch kiểm soát caching mà không cần động đến response headers:
const response = await fetch(request, {
cf: {
cacheTtl: 86400, // cache 24h bất kể headers
cacheEverything: true, // cache HTML và các loại không mặc định khác
cacheTtlByStatus: { // TTL riêng theo status code
"200-299": 86400,
"404": 60,
"500-599": 0
},
cacheTags: ["product-images"] // để purge theo tag
}
});
Dùng tùy chọn cf khi muốn behavior đặc thù của Cloudflare mà không làm ảnh hưởng đến response headers mà browser nhìn thấy.
Bước 3: Xác minh với cf-cache-status
Sau khi deploy, kiểm tra xem headers có thực sự hoạt động không.
# Request đầu tiên — cache chưa có, phải là MISS
curl -sI https://yoursite.com/assets/app.abc123.js | grep cf-cache-status
# cf-cache-status: MISS
# Request thứ hai từ cùng region — phải là HIT
curl -sI https://yoursite.com/assets/app.abc123.js | grep cf-cache-status
# cf-cache-status: HIT
# Xác minh Cache-Control thực tế trên fingerprinted assets
curl -sI https://yoursite.com/assets/app.abc123.js | grep cache-control
# cache-control: public, max-age=31536000, immutable
# Xác minh no-cache trên index.html
curl -sI https://yoursite.com/ | grep cache-control
# cache-control: no-cache
# Kiểm tra Set-Cookie trên static assets (nếu có là vấn đề)
curl -sI https://yoursite.com/assets/app.abc123.js | grep -iE "set-cookie|cf-cache-status"
Trong DevTools của trình duyệt: tab Network → chọn asset → Response Headers → tìm cf-cache-status.
Tất cả các giá trị của cf-cache-status:
| Giá trị | Ý nghĩa |
|---|---|
HIT | Được serve từ Cloudflare edge — lý tưởng |
MISS | Không có trong cache; fetch từ origin |
EXPIRED | Đã cache nhưng hết hạn; fetch mới từ origin |
REVALIDATED | Đã validate với origin (304), serve từ cache |
UPDATING | Đang serve nội dung cũ trong khi fetch mới ở nền |
STALE | Serve nội dung cũ; origin không truy cập được |
BYPASS | Origin yêu cầu Cloudflare không cache |
DYNAMIC | Asset không đủ điều kiện cache; không có rule nào bắt buộc |
NONE/UNKNOWN | Được tạo bởi Workers, WAF, hoặc redirect rules |
Nếu thấy BYPASS trên file .js hoặc .css, phần lỗi thường gặp bên dưới sẽ giải thích tại sao.
Lỗi thường gặp
Đây là những vấn đề cụ thể tạo ra BYPASS hoặc DYNAMIC trên các assets đáng lẽ phải là HIT.
Bẫy Set-Cookie
Nếu origin (hoặc bất kỳ middleware nào trong stack) set header Set-Cookie trong response — kể cả file .js tĩnh — Cloudflare sẽ bypass cache và trả về cf-cache-status: BYPASS. Đây là behavior tuân thủ RFC 7234, nhưng nó khiến mọi developer ngạc nhiên lần đầu tiên gặp.
Cách sửa: đảm bảo không có middleware nào set cookies trên static asset paths. Nếu không thể thay đổi behavior của origin, tạo một Cache Rule chỉ áp dụng cho /assets/* và loại bỏ cookies khỏi cache key.
DYNAMIC vs. BYPASS
Hai giá trị này khác nhau. DYNAMIC có nghĩa là asset chưa bao giờ được xem là có thể cache — loại file không hợp lệ, hoặc Cache Rule có “Bypass cache” khớp quá sớm. BYPASS có nghĩa là asset đủ điều kiện nhưng origin yêu cầu Cloudflare không cache. Nếu thấy DYNAMIC trên file .js, kiểm tra xem có Cache Rule quá rộng nào đang kích hoạt trên pattern bao gồm cả asset paths của bạn không.
Dashboard Browser Cache TTL ghi đè origin headers
Setting Browser Cache TTL của Cloudflare trong dashboard có thể ghi đè max-age từ origin. Bạn set max-age 1 năm trong origin headers, nhưng nếu dashboard Browser Cache TTL đang ở “4 hours,” browser sẽ cache trong 4 giờ. Luôn đồng bộ setting trong dashboard với origin headers, hoặc để ở “Respect Existing Headers.”
Cache Reserve có purge eventual-consistency
Nếu bạn đang dùng Cache Reserve (persistent edge cache dựa trên R2 của Cloudflare), các thao tác purge-by-tag không diễn ra ngay lập tức. Các quy trình deploy phụ thuộc vào purge sẽ tạm thời serve nội dung cũ. Cách tốt hơn: dùng content-hashed filenames. Vite, Astro, và Next.js đều tạo hashed filenames theo mặc định. Hash thay đổi khi nội dung thay đổi; bạn không bao giờ cần purge.
immutable chỉ có tác dụng với browser
immutable là browser directive. Edge của Cloudflare bỏ qua nó. Behavior tại edge hoàn toàn được kiểm soát bởi max-age và s-maxage. Vẫn nên thêm immutable vào headers — nó loại bỏ conditional GET requests phía browser trên fingerprinted assets — nhưng đừng kỳ vọng nó ảnh hưởng đến Cloudflare.
Service workers phải dùng no-store
Browser rất tích cực trong việc cache service workers. Một /sw.js đã cache trỏ đến asset hash cũ có thể serve ứng dụng bị hỏng vô thời hạn. Cách sửa là Cache-Control: no-store trên path của service worker. Đây là trường hợp duy nhất no-store là giá trị đúng, không phải no-cache.
Tóm tắt
Các quy tắc cốt lõi:
| Loại asset | Cache-Control đúng |
|---|---|
| Fingerprinted JS/CSS/images | public, max-age=31536000, immutable |
index.html, entry points | no-cache |
Service worker (/sw.js) | no-store |
| API / user-specific responses | private, no-cache |
Với Pages: đặt file _headers trong thư mục build output. Với Workers: set headers trên Response object hoặc dùng tùy chọn cf của fetch.
Sau khi deploy, chạy curl -sI để kiểm tra assets và xác nhận cf-cache-status: HIT ở request thứ hai. Nếu thấy BYPASS, kiểm tra Set-Cookie trong response. Nếu thấy DYNAMIC, kiểm tra xem có Cache Rule quá rộng không.
Content-hashed filenames giúp tất cả dễ hơn nhiều. Để Vite, Astro, hoặc Next.js xử lý việc hash, rồi cache tích cực mà không lo về thời điểm purge.
Với Workers production, rate limiting Cloudflare Workers không cần Redis là bước tiếp theo tự nhiên sau khi đã cấu hình xong caching.