# 🌐 CDN cho static assets và cache busting

By Better Wise

Bài trước bạn đã đặt `Cache-Control`, `ETag`, `Vary` lên response và biết rằng `max-age` dài thì nhanh nhưng dễ cũ. Bài này lấy đúng bộ header đó áp cho static assets (JS, CSS, ảnh, font) khi có CDN ở giữa, và trả lời câu hỏi khó chịu nhất: làm sao vừa cache một năm vừa phát hành bản mới ngay khi deploy?

**Mục tiêu bài này:**
- Giải thích CDN làm gì với static assets, bằng ví dụ tủ lạnh.
- Đặt `max-age` dài kèm `immutable` cho file có hash trong tên.
- Đặt hash nội dung vào tên file để mỗi bản phát hành là một URL mới.
- Biết vì sao `index.html` phải đi con đường ngược lại.
- Tránh bẫy `?v=2` và biết thứ tự deploy an toàn.

## CDN là tủ lạnh đặt ở mỗi tòa nhà

Origin server là cái chợ. CDN là tủ lạnh đặt ở tầng trệt của mỗi tòa nhà (edge). Lần đầu ai đó cần sữa thì vẫn phải đi chợ, tức cache miss, edge về origin lấy hàng. Những lần sau mở tủ là có, tức cache hit tại edge, và user ở Hà Nội không phải chờ gói tin đi vòng sang Mỹ.

Static assets hợp với mô hình này hơn bất cứ thứ gì: cùng một nội dung cho mọi user (không phụ thuộc cookie hay user id), ít thay đổi, nhưng được đọc rất nhiều lần.

```mermaid
flowchart TD
    Browser["👤<br/>Trình duyệt"] --> Edge{"🧊<br/>Edge cache"}
    Edge -->|"HIT"| Browser
    Edge -->|"MISS"| Origin["🏠<br/>Origin server"]
    Origin --> Edge
```

Vì cache key ở edge là URL (cộng thêm vài header trong `Vary`), nên câu hỏi thiết kế thật ra rất gọn: **làm sao để nội dung mới có URL mới**.

## Cache lâu và phát hành nhanh: tách bằng URL

Hai mong muốn này đối nghịch nhau nếu bạn gắn cache vào một URL cố định. `max-age` dài thì user không thấy bản mới, `max-age` ngắn thì mất lợi ích của CDN. Cách thoát là đổi chỗ neo: không neo cache vào "file tên gì" mà neo vào "nội dung nào".

Nếu tên file chứa hash của nội dung, ví dụ `app.3f9c1a2b.js`, thì khi code đổi, hash đổi, URL đổi. File cũ vẫn nằm trong cache nhưng không còn ai trỏ tới. Với URL như vậy bạn được phép đặt:

```
Cache-Control: public, max-age=31536000, immutable
```

- `max-age=31536000` là _____ (31.536.000 giây).
- `_____` báo trình duyệt rằng trong khoảng đó đừng revalidate, kể cả khi user bấm F5, vì nội dung sau URL này sẽ không bao giờ đổi.

Đây là điểm khác biệt lớn so với bài trước: `ETag` vẫn hữu ích, nhưng với asset có hash thì ta cố tình không muốn phát sinh request revalidate nào cả.

## Hash trong tên file: cache busting bền vững

Cache busting nghĩa là làm cho URL thay đổi khi nội dung thay đổi, để "đập" cache một cách có chủ đích. Tên file nên chứa _____ chứ không phải số version do người viết tay, vì hash chỉ đổi khi file thật sự đổi.

Một bảng nhỏ đủ dùng cho hầu hết dự án:

| Asset | Header gợi ý | Lý do |
| --- | --- | --- |
| `index.html` | `no-cache` | luôn revalidate, phải thấy hash mới |
| `app.<hash>.js`, `style.<hash>.css` | `public, max-age=31536000, immutable` | URL bất biến |
| `logo.<hash>.png`, font | như trên | ảnh và font gần như không đổi |
| `favicon.ico` (không hash) | `public, max-age=86400` | chấp nhận trễ một ngày |
| API response | `no-store` hoặc TTL ngắn | dữ liệu riêng của từng user |

## HTML thì phải đi ngược lại

`index.html` là thứ trỏ tới các hash. Nếu HTML bị cache một năm, user sẽ mãi thấy HTML cũ trỏ tới `app.3f9c1a2b.js` cũ, và bản deploy mới coi như vô hình. Nên HTML dùng `_____`, nghĩa là vẫn được lưu nhưng mỗi lần dùng phải hỏi lại server. Nhờ `ETag`, lần hỏi lại đó thường chỉ là một `304 Not Modified` vài chục byte.

Đừng nhầm `no-cache` với `no-store`: `no-cache` cho phép lưu và bắt revalidate, còn `no-store` là không lưu gì cả, và bạn mất luôn lợi ích 304.

## Bẫy `?v=2` và thứ tự deploy

Cách cũ là `style.css?v=2`. Nó chạy được, nhưng nhiều CDN được cấu hình bỏ qua query string khi tính cache key (để gộp các URL tracking), nên `?v=2` vẫn trả bản cũ. Hash trong path không bị bẫy này.

Thứ tự deploy cũng là một phần của cache busting. Asset phải lên _____, HTML lên sau:

1. Build sinh ra `app.3f9c1a2b.js` với hash mới
2. Upload asset mới lên origin, để CDN kéo về với max-age dài
3. Kiểm tra asset mới trả 200 từ CDN
4. Cuối cùng upload `index.html` mới trỏ tới hash mới
5. Giữ asset cũ trên CDN thêm vài ngày, đừng xóa ngay

Lý do của thứ tự này rất thực tế: nếu HTML mới lên trước, user tải HTML trỏ tới `app.3f9c1a2b.js` trong khi file đó chưa tồn tại, kết quả là 404 và trang trắng. Ngược lại thì luôn an toàn vì HTML cũ vẫn trỏ tới file cũ còn đó.

Ví dụ rollback: một user đang mở tab từ sáng, HTML cũ trỏ `app.1a2b3c4d.js`. Bạn deploy bản mới rồi xóa asset cũ. User F5 và nhận 404. Giữ asset cũ thì rollback chỉ đơn giản là đưa `index.html` cũ trở lại.

## Kiểm tra bằng mắt: đọc header từ CDN

Cache busting sai thường không báo lỗi, nó chỉ lặng lẽ trả bản cũ. Cách nhanh nhất là nhìn header:

```
$ curl -I https://cdn.example.com/app.3f9c1a2b.js
cache-control: public, max-age=31536000, immutable
x-cache: HIT
age: 84321
```

`x-cache: HIT` nghĩa là edge đang phục vụ từ cache, không về origin. `age: 84321` là số giây object đã nằm trong cache, so với `max-age` để biết khi nào nó hết hạn. Nếu bạn thấy `age` gần bằng `max-age` trong khi vừa deploy bản mới, gần như chắc chắn bạn đang thiếu hash trong tên file.

## Những lỗi khiến CDN vẫn trả bản cũ

- **Đặt `immutable` cho file không có hash.** User kẹt bản cũ suốt một năm và F5 không cứu được, vì `immutable` bảo trình duyệt đừng hỏi lại. Quy tắc: tên file không đổi thì đừng cho `max-age` dài.
- **Xóa asset cũ ngay sau khi deploy.** Tab đang mở và bản rollback đều cần file cũ. Giữ ít nhất vài ngày, hoặc theo TTL của `index.html`.
- **Purge toàn bộ CDN mỗi lần deploy.** Purge là van xả khẩn cấp, không phải quy trình. Nếu phải purge thường xuyên, vấn đề nằm ở tên file, không nằm ở CDN.
- **Cache HTML quá lâu.** Đây là lỗi tốn thời gian debug nhất, vì mọi thứ trên CDN đều đúng, chỉ có danh sách hash là cũ.
- **Đoán thay vì đo.** Trước khi kết luận, hãy `curl -I` một file asset và một file HTML, so `cache-control` và `x-cache` với bảng ở trên.

## Chốt lại ba dòng

- Static assets nên có `max-age` rất dài (một năm) kèm `immutable`, và điều kiện bắt buộc là hash nội dung nằm trong tên file.
- `index.html` dùng `no-cache` để luôn revalidate, nhờ đó user nhận được danh sách hash mới mà không phải tải lại toàn bộ.
- Deploy theo thứ tự asset trước, HTML sau, giữ asset cũ vài ngày; cache busting là thiết kế URL, không phải thao tác purge.

Bài tiếp theo là checkpoint của module: bạn sẽ gặp lại TTL, invalidation theo sự kiện, `Cache-Control`, `ETag`, `Vary` và chính cách đặt hash này trong một bộ câu hỏi tình huống.

Front: Vì sao static assets là ứng viên số một cho CDN?
Back: Cùng một nội dung cho mọi user, ít thay đổi, nhưng được đọc rất nhiều lần.

Front: max-age=31536000 tương đương bao lâu?
Back: Một năm (31.536.000 giây).

Front: Header immutable nói gì với trình duyệt?
Back: Đừng revalidate trong thời gian max-age, kể cả khi user bấm F5.

Front: Điều kiện bắt buộc để được đặt max-age một năm kèm immutable?
Back: Tên file chứa hash nội dung, nên URL đó không bao giờ đổi nội dung.

Front: Vì sao index.html nên dùng no-cache?
Back: Vì nó trỏ tới các hash mới, cần revalidate để user thấy bản deploy mới.

Front: no-cache khác no-store ở điểm nào?
Back: no-cache vẫn lưu nhưng bắt revalidate; no-store không lưu gì cả.

Front: Cơ chế nào giúp lần revalidate của HTML thường chỉ tốn một 304?
Back: ETag: server trả 304 Not Modified nếu nội dung chưa đổi.

Front: Cache busting bằng hash trong tên file khác ?v=2 thế nào?
Back: Hash nằm trong path nên CDN không thể bỏ qua; nhiều CDN cấu hình bỏ query string khi tính cache key.

Front: Vì sao phải upload asset trước rồi mới upload HTML?
Back: Tránh HTML mới trỏ tới file chưa tồn tại, gây 404 và trang trắng.

Front: Vì sao giữ asset cũ trên CDN thêm vài ngày?
Back: Tab đang mở với HTML cũ vẫn trỏ tới file đó, và rollback cần nó.

Front: x-cache: HIT trong response của CDN nghĩa là gì?
Back: Edge trả nội dung từ cache, không phải về origin.

Front: Header age: 84321 cho biết điều gì?
Back: Object đã nằm trong cache 84.321 giây, so với max-age để biết khi nào hết hạn.

Front: Đặt immutable cho file không có hash thì hậu quả là gì?
Back: User kẹt bản cũ suốt max-age và F5 không cứu được.

---

## Contents

Part of [🗄️ Caching căn bản: Redis, TTL và CDN cho người mới](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi.md)

- **🧱 Nền tảng cache: vì sao và khi nào**
  - [💡 Cache là gì và khi nào nên dùng](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cache-la-gi-va-khi-nao-nen-dung.md)
  - [🔁 Cache-aside: luồng đọc ghi kèm ví dụ](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cache-aside-luong-doc-ghi-voi-vi-du.md) (sign-in required)
  - [✅ Checkpoint: Nền tảng cache](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-nen-tang-cache.md) (sign-in required)
- **⏱️ TTL, invalidation và cache phía HTTP**
  - [🗑️ TTL và invalidation: chọn thời điểm xóa](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/ttl-va-invalidation-chon-thoi-diem-xoa.md)
  - [📨 HTTP cache headers: Cache-Control, ETag, Vary](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/http-cache-headers-cache-control-etag-vary.md)
  - [🌐 CDN cho static assets và cache busting](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cdn-cho-static-assets-va-cache-busting.md) — this lesson
  - [✅ Checkpoint: TTL và cache HTTP](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-ttl-va-cache-http.md)
- **⚠️ Redis và các lỗi cache thường gặp**
  - [🧰 Redis nhập môn: GET, SET, TTL và key naming](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/redis-nhap-mon-get-set-ttl-va-key-naming.md)
  - [🚨 Stale data và cache stampede](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/stale-data-va-cache-stampede.md)
  - [🤖 Yêu cầu AI thêm cache mà không phá logic](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/yeu-cau-ai-them-cache-khong-pha-logic.md) (premium content)
  - [✅ Checkpoint: Redis và lỗi cache](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/checkpoint-redis-va-loi-cache.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/caching-can-ban-redis-ttl-va-cdn-cho-nguoi-moi/cheatsheet.md) (premium content)
