Next.js 16 from Zero to Senior · Part 4 — Cache Components & the New Caching Model
The headline feature of Next.js 16: Cache Components. Enable cacheComponents, use the "use cache" directive, control TTL with cacheLife, invalidate with cacheTag + revalidateTag/updateTag, and ship instant pages with PPR.
Đây là phần khiến Next.js 16 khác mọi thứ trước đó. Nếu chỉ học sâu một chương, hãy là chương này — caching là nơi phần lớn bug và phần lớn hiệu năng của Next.js nằm.
Ở Next.js 13–15, caching là ngầm định: fetch được cache mặc định, cộng một mớ lộn xộn revalidate, unstable_cache, và cache(). Next.js 16 vứt bỏ tất cả và thay bằng một primitive rõ ràng, kết hợp được: Cache Components.
1. Mô hình tư duy
Ba directive giờ mô tả code chạy ở đâu và được cache thế nào:
'use client' → this code runs in the browser
'use server' → this code is a server action callable from the client
'use cache' → this code's result is cached and reused
'use cache' là người anh em về caching của 'use client' và 'use server'. Caching trở thành một quyết định rõ ràng, kiểm tra được — bạn có thể grep codebase tìm 'use cache' và thấy chính xác cái gì được cache.
Mặc định ngược với trước đây: không gì được cache; mọi thứ động lúc request trừ khi bạn chủ động chọn.
2. Bật nó lên
Cache Components bật ở cấp project. Không có cờ này, 'use cache' không làm gì (nó là no-op âm thầm, ở cả dev lẫn production):
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
Ở 16.2 API đã tốt nghiệp từ
unstable_lên ổn định. Bài cũ hiệnunstable_cacheLife/dynamicIO— trên 16.2+ hãy dùng tên ổn định bên dưới.
3. 'use cache' — ba phạm vi
Đặt directive ở đầu một hàm, một component, hoặc cả một file.
Phạm vi hàm
// lib/products.ts
export async function getProducts() {
'use cache';
const res = await fetch('https://api.example.com/products');
return res.json();
}
Phạm vi component
async function ProductList() {
'use cache';
const products = await getProductsFromDb();
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Phạm vi file
// app/blog/page.tsx
'use cache'; // every export in this file is cacheable
export default async function BlogIndex() {
const posts = await getAllPosts();
return <PostList posts={posts} />;
}
Trình biên dịch tự động tạo một cache key từ tham số của hàm và closure nó bắt được. Gọi getProduct(42) và getProduct(7) thì chúng cache riêng, theo tham số.
Vì tham số trở thành cache key, mọi thứ truyền vào phải serialize được, và bạn không thể đọc dữ liệu request (
cookies(),headers(),searchParams) trong phạm vi'use cache'— làm vậy sẽ khiến kết quả không cache được.
4. cacheLife — cache bao lâu
Mặc định một entry cache dùng một profile hợp lý. Để kiểm soát độ tươi, gọi cacheLife với một profile có tên:
import { cacheLife } from 'next/cache';
export async function getPrice() {
'use cache';
cacheLife('minutes'); // built-in profile
const res = await fetch('https://api.example.com/price');
return res.json();
}
Các profile dựng sẵn gồm seconds, minutes, hours, days, weeks, và max. Mỗi profile định nghĩa ba con số:
- client được dùng giá trị cache bao lâu mà không kiểm tra.
- server làm mới ngầm bao thường xuyên.
- tuổi tối đa trước khi buộc fetch lại.
Định nghĩa profile tùy chỉnh trong config:
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
pricing: { stale: 60, revalidate: 300, expire: 3600 }, // seconds
},
};
cacheLife('pricing'); // use your custom profile
5. cacheTag — vô hiệu hóa có chủ đích
Gắn tag cho một entry cache để sau này vô hiệu hóa chính xác:
import { cacheTag } from 'next/cache';
export async function getProduct(id: string) {
'use cache';
cacheTag(`product-${id}`, 'products'); // attach one or more tags
return db.product.findUnique({ where: { id } });
}
Giờ khi một sản phẩm đổi, vô hiệu hóa đúng tag đó thay vì xóa toàn bộ cache.
revalidateTag so với updateTag
Cả hai vô hiệu hóa theo tag, nhưng khác ở khi nào dữ liệu mới hiện ra:
import { revalidateTag, updateTag } from 'next/cache';
// In a Server Action after a mutation:
revalidateTag('products'); // marks stale; next request refetches
updateTag('products'); // refreshes immediately AND shows fresh data
// in the SAME response (read-your-writes)
- đánh dấu entry là cũ; người xem tiếp theo kích hoạt làm mới. Tốt cho dữ liệu công khai “nhất quán dần”.
- (mới ở 16) vô hiệu hóa và render lại ngay trong response của action hiện tại, nên người vừa thay đổi thấy cập nhật của chính mình tức thì. Cái này giải quyết bug kinh điển “tôi vừa submit form mà vẫn thấy dữ liệu cũ”.
Còn có revalidatePath('/products') để vô hiệu hóa mọi thứ render trên một path.
6. Partial Prerendering (PPR) — phần thưởng
Cache Components xây trên PPR, kết thúc thế lưỡng nan cũ “trang này tĩnh HAY động?”. Giờ một trang có thể cả hai:
- Component đánh dấu
'use cache'trở thành vỏ tĩnh — prerender sẵn, phục vụ tức thì từ edge. - Component đọc dữ liệu request (hoặc không cache) trở thành lỗ động, bọc trong
<Suspense>và stream vào.
// app/product/[id]/page.tsx
import { Suspense } from 'react';
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<>
{/* Static shell: cached, instant */}
<ProductDetails id={id} />
{/* Dynamic hole: personalized, streamed */}
<Suspense fallback={<RecsSkeleton />}>
<Recommendations id={id} />
</Suspense>
</>
);
}
async function ProductDetails({ id }: { id: string }) {
'use cache';
cacheTag(`product-${id}`);
const product = await getProduct(id);
return <h1>{product.name}</h1>;
}
async function Recommendations({ id }: { id: string }) {
// reads cookies for personalization → dynamic, streamed in a Suspense hole
const recs = await getPersonalizedRecs(id);
return <RecList recs={recs} />;
}
User nhận tiêu đề sản phẩm tức thì (vỏ tĩnh) trong khi gợi ý cá nhân hóa stream vào một lúc sau. Tốt nhất của cả hai.
Nếu một trang có phần động không cache mà không bọc trong
<Suspense>, Next.js 16 sẽ cảnh báo lúc build — Suspense là cách bạn khai báo “lỗ này được phép động”.
7. Di trú từ mô hình cũ
Nếu bạn từng thấy những thứ này trong code cũ, đây là cái thay thế ở Next.js 16:
OLD (13–15) NEW (16)
─────────────────────────────────────────────────────────────────
fetch(url) cached by default → fetch(url) is dynamic; add 'use cache'
export const revalidate = 60 → cacheLife() inside a 'use cache' scope
unstable_cache(fn, keys, { tags }) → 'use cache' + cacheTag() (unstable_cache removed)
fetch(url, { next: { tags } }) → cacheTag() in a 'use cache' function
fetch(url, { next: { revalidate } }) → cacheLife() in a 'use cache' function
Nâng cấp về tư duy: caching không còn gắn vào các tùy chọn fetch rải rác — nó là thuộc tính của một hàm hoặc component, khai báo một lần ở đầu.
8. Hướng dẫn quyết định
| Dữ liệu | Chiến lược |
|---|---|
| Nội dung marketing, docs | 'use cache' + cacheLife('days') |
| Danh mục sản phẩm | 'use cache' + cacheTag + revalidateTag on edit |
| Giá đổi thường xuyên | 'use cache' + cacheLife('minutes') |
| Dashboard riêng user | dynamic (no cache), stream in <Suspense> |
| Đọc cookie/header | dynamic — can’t be cached by definition |
| Sau khi ghi, hiện data mới ngay | updateTag in the Server Action |
Bản năng senior: cache những thứ dùng chung, ít đổi; stream những thứ cá nhân, đổi nhanh.
9. Bài tập
-
Bật & kiểm chứng: bật cờ, thêm
'use cache'vàogetProducts()có log bên trong. Reload vài lần xác nhận log chạy một lần, rồi tắt cờ và xác nhận nó log mỗi lần. -
cacheLife: cache một timestamp với
cacheLife('seconds')và xem nó chỉ cập nhật sau khi cửa sổ của profile trôi qua. -
Gắn tag & vô hiệu hóa: gắn tag sản phẩm bằng
cacheTag('product-1'), rồi thêm nút Server Action gọirevalidateTag('product-1')và xác nhận giá trị cache làm mới. -
updateTag vs revalidateTag: dựng cùng luồng sửa hai lần — một với
revalidateTag, một vớiupdateTag— và quan sát cái nào hiện data mới trong cùng lần submit. -
PPR: dựng trang sản phẩm với vỏ
<ProductDetails>đã cache và<Recommendations>động trong<Suspense>. Chạynext buildvà xác nhận trang được prerender một phần.
Phần tiếp theo
Giờ bạn hiểu phần quan trọng nhất — và bị hiểu nhầm nhiều nhất — của Next.js: khi nào code chạy, khi nào kết quả được cache, cách vô hiệu hóa chính xác, và cách PPR cho bạn vỏ tức thì với lỗ động.
Phần 5 chuyển sang trình duyệt: Client Components, tương tác, và dùng URL làm state với useRouter, usePathname, và useSearchParams.