Bộ nhớ đệm và xác thực lại
Cấu hình phân đoạn route, unstable_cache, revalidateTag / revalidatePath và stale-while-revalidate.
Lưu phản hồi của route handler và dữ liệu vào bộ nhớ đệm, rồi xác thực lại theo lịch hoặc theo yêu cầu. Các mục cũ được trả về ngay trong khi một lần làm mới chạy nền.
Route handler
Route handler GET mặc định là động. Bật bộ nhớ đệm bằng cấu hình phân đoạn route:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Export | Hành vi |
|---|---|
| (không có) | Chạy với mọi yêu cầu |
dynamic = "force-static" | Lưu vào bộ nhớ đệm đến khi xác thực lại |
revalidate = N | Lưu vào bộ nhớ đệm; trở thành cũ sau N giây |
revalidate = 0 hoặc dynamic = "force-dynamic" | Chạy với mọi yêu cầu |
Các giá trị dynamic khác gây lỗi TX031. Trong đó có "error" của Next.js, vì Text.js không thể phát hiện khi handler đọc yêu cầu.
Handler có bộ nhớ đệm có một mục cho mỗi pathname, và request của chúng giống như tại thời điểm build: không có chuỗi truy vấn, header, cookie hay phần thân. Thay vào đó, đọc chúng trong handler động. Chỉ GET và HEAD dùng bộ nhớ đệm. Phản hồi có trạng thái 5xx, header Set-Cookie, hoặc Cache-Control: private / no-store vẫn trả về nhưng không bao giờ được lưu.
Header
Phản hồi từ bộ nhớ đệm có:
| Header | Giá trị |
|---|---|
x-textjs-cache | MISS, HIT hoặc STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N>, hoặc s-maxage=31536000 cho force-static |
Khi middleware đặt cookie, viết lại hoặc ghi đè header của yêu cầu, header được tạo trở thành private, no-cache, no-store, max-age=0, must-revalidate, nên bộ nhớ đệm dùng chung không bao giờ phát lại quyết định dành riêng cho từng khách truy cập. Header Cache-Control do handler đặt vẫn giữ nguyên.
Trong text dev, cấu hình phân đoạn vẫn áp dụng nhưng không lưu gì, nên mọi yêu cầu đều chạy handler.
Bộ nhớ đệm dữ liệu
Lưu kết quả của bất kỳ hàm bất đồng bộ nào vào bộ nhớ đệm bằng unstable_cache từ @textjs/app/cache:
import { unstable_cache } from "@textjs/app/cache";export const getPosts = unstable_cache( async (author: string) => db.posts.byAuthor(author), ["posts"], { tags: ["posts"], revalidate: 300 },);Khóa gồm các phần khóa cộng với đối số, mã hóa theo giá trị (các đối số undefined và null, Map, Set, Date và URL vẫn phân biệt với nhau). Nếu không có các phần khóa, mã nguồn của hàm và nơi tạo hàm xác định danh tính của nó; truyền các phần khóa mỗi khi hàm giữ giá trị trong closure, ví dụ khi tạo hàm cho từng yêu cầu hoặc trong vòng lặp. Kết quả được tuần tự hóa thành JSON, nên mỗi lần gọi trả về một bản sao mới (và đối tượng Date trở về dưới dạng chuỗi); kết quả không thể tuần tự hóa sẽ gây lỗi. cache(fn, { key, tags, revalidate }) nhận cùng các thiết lập dưới dạng đối tượng tùy chọn.
Bên trong route handler có bộ nhớ đệm, các mục dữ liệu truyền thẻ và revalidate ngắn nhất của chúng lên route: xác thực lại "posts" cũng làm mới /api/posts. Lời gọi dữ liệu với revalidate: 0 khiến route trở thành động.
Trả dữ liệu cũ trong khi xác thực lại
Khi tuổi của một mục vượt quá revalidate:
- Yêu cầu tiếp theo nhận ngay giá trị cũ (
STALE). - Một lần làm mới chạy nền; các yêu cầu đồng thời dùng chung lần làm mới đó.
- Nếu lần làm mới gây lỗi, giá trị cũ tiếp tục được trả về và lỗi được ghi vào nhật ký.
Trên Vercel và Cloudflare, các lần làm mới nền được đăng ký bằng waitUntil(). Các mục cũ hơn một năm luôn được tính lại ở tiến trình chính.
Xác thực lại theo yêu cầu
import { revalidatePath, revalidateTag } from "@textjs/app/cache";export async function POST(request: Request) { // Anyone can POST here: require a secret shared with your CMS or webhook. if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) { return new Response("Unauthorized", { status: 401 }); } await revalidateTag("posts"); await revalidatePath("/api/feed"); return Response.json({ revalidated: true });}| Lời gọi | Phạm vi vô hiệu hóa |
|---|---|
revalidateTag("posts") | Mọi thứ gắn thẻ posts; yêu cầu tiếp theo tính lại |
revalidateTag("posts", "max") | Đánh dấu là cũ; yêu cầu tiếp theo nhận dữ liệu cũ trong khi dữ liệu làm mới |
revalidatePath("/api/feed") | Đường dẫn đó |
revalidatePath("/api/posts/[id]") | Mọi đường dẫn của một route động |
revalidatePath("/api", "layout") | Mọi thứ bên dưới /api |
Cả hai đều trả về promise. Với kho lưu trữ trong bộ nhớ, vô hiệu hóa có hiệu lực ngay; dùng await khi kho lưu trữ tùy chỉnh là bất đồng bộ.
Kho lưu trữ bộ nhớ đệm tùy chỉnh
Kho lưu trữ mặc định là LRU trong bộ nhớ (1.000 mục, khoảng 50 MB) cho mỗi phiên bản máy chủ. Để dùng chung bộ nhớ đệm giữa các phiên bản, trỏ cacheHandler đến một mô-đun xuất mặc định một CacheStore:
export default defineConfig({ cacheHandler: "./cache-handler.ts" });import type { CacheStore } from "@textjs/app/cache";const store: CacheStore = { async get(key) { return redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)); }, async set(key, entry) { await redis.set(key, JSON.stringify(entry)); }, async delete(key) { await redis.del(key); }, async markTags(tags, mode, at) { for (const t of tags) await redis.hset(`tag:${t}`, mode, at); }, async getTagState(tags) { /* max expiredAt / staleAt across tags */ return { expiredAt: 0, staleAt: 0 }; },};export default store;Các mục là JSON thuần. Thay vào đó, setCacheStore(store) cài đặt kho lưu trữ tại thời điểm chạy.
Lưu ý về nền tảng
- Mỗi phiên bản serverless (Vercel functions, Cloudflare isolates) có bộ nhớ đệm riêng trong bộ nhớ.
Cache-Control: s-maxagecho phép CDN của nền tảng lưu phản hồi vào bộ nhớ đệm; xác thực lại theo yêu cầu không xóa bộ nhớ đệm CDN. DùngcacheHandlerchung khirevalidateTagcần tác động đến mọi phiên bản. - Cloudflare yêu cầu cờ
nodejs_compat(bộ nhớ đệm dùngAsyncLocalStorage). Xem Triển khai.