· 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

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 statusEdge TTL
200, 206, 301120 phút
302, 30320 phút
404, 4103 phút

Khi Cloudflare sẽ không cache, bất kể loại file:

  • Cache-Control: private, no-store, no-cache, hoặc max-age=0 có 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 BYPASSDYNAMIC.

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à browser
  • max-age=31536000 — 1 năm trong browser
  • immutable — 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:

  1. Tối đa 100 header rules mỗi file _headers.
  2. Giới hạn 2.000 ký tự mỗi dòng.
  3. _headers khô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 _headers sẽ bị bỏ qua cho route đó. Thay vào đó, hãy set headers trong Response củ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
MISSKhô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
STALEServe nội dung cũ; origin không truy cập được
BYPASSOrigin yêu cầu Cloudflare không cache
DYNAMICAsset 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.

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-ages-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 assetCache-Control đúng
Fingerprinted JS/CSS/imagespublic, max-age=31536000, immutable
index.html, entry pointsno-cache
Service worker (/sw.js)no-store
API / user-specific responsesprivate, 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.

Tài liệu tham khảo