快取與重新驗證
路由區段設定、unstable_cache、revalidateTag / revalidatePath 與 stale-while-revalidate。
快取路由處理常式的回應與資料,再定時或依需求重新驗證。執行一次背景更新時,立即提供過期的項目。
路由處理常式
GET 路由處理常式預設為動態。使用路由區段設定啟用快取:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| 匯出 | 行為 |
|---|---|
| (無) | 每次請求都執行 |
dynamic = "force-static" | 快取至重新驗證為止 |
revalidate = N | 快取;N 秒後過期 |
revalidate = 0 或 dynamic = "force-dynamic" | 每次請求都執行 |
其他 dynamic 值會觸發 TX031 錯誤。這也包括 Next.js 的 "error",因為 Text.js 無法偵測處理常式何時讀取請求。
快取的處理常式會為每個路徑名稱建立一個項目,其 request 如同建構時的請求:沒有查詢字串、標頭、Cookie 或主體。請改在動態處理常式中讀取這些內容。只有 GET 和 HEAD 會快取。帶有 5xx 狀態、Set-Cookie 標頭或 Cache-Control: private / no-store 的回應會回傳,但絕不儲存。
標頭
快取的回應帶有:
| 標頭 | 值 |
|---|---|
x-textjs-cache | MISS、HIT 或 STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N>,或在 force-static 時使用 s-maxage=31536000 |
當中介軟體為請求設定 cookie、重寫或覆寫請求標頭時,產生的標頭會變成 private, no-cache, no-store, max-age=0, must-revalidate,因此共用快取不會重複使用針對個別訪客做出的決定。處理常式設定的 Cache-Control 標頭會保留。
在 text dev 中,區段設定仍然生效,但不會儲存任何內容,因此每次請求都會執行處理常式。
資料快取
使用 @textjs/app/cache 的 unstable_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 },);快取鍵由鍵的組成部分加上引數組成,依值編碼(undefined 與 null、Map、Set、Date 和 URL 引數各自保持區別)。未提供鍵的組成部分時,會以函式的原始碼與建立位置識別函式;只要函式透過閉包擷取值,就應傳入鍵的組成部分,例如為每次請求建立函式或在迴圈中建立函式時。結果會以 JSON 序列化,因此每次呼叫都會回傳新的副本(而 Date 物件會以字串回傳);無法序列化的結果會擲回錯誤。cache(fn, { key, tags, revalidate }) 透過選項物件接受相同設定。
在快取的路由處理常式中,資料項目會將標籤與最短的 revalidate 傳遞給路由:重新驗證 "posts" 也會更新 /api/posts。帶有 revalidate: 0 的資料呼叫會使路由變為動態。
重新驗證期間提供過期資料
當項目的存留時間超過 revalidate 時:
- 下一個請求會立即收到過期值(
STALE)。 - 背景執行一次更新;並行請求共用這次更新。
- 如果更新擲回錯誤,會繼續提供過期值並記錄錯誤。
在 Vercel 和 Cloudflare 上,背景更新會透過 waitUntil() 註冊。超過一年的項目一律在前景重新計算。
依需求重新驗證
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 });}| 呼叫 | 失效範圍 |
|---|---|
revalidateTag("posts") | 所有標記為 posts 的項目;下一個請求會重新計算 |
revalidateTag("posts", "max") | 標記為過期;下一個請求會在更新期間收到過期值 |
revalidatePath("/api/feed") | 該路徑 |
revalidatePath("/api/posts/[id]") | 動態路由的每個路徑 |
revalidatePath("/api", "layout") | /api 底下的所有內容 |
兩者都會回傳 Promise。使用記憶體儲存區時會立即失效;若自訂儲存區 為非同步,請使用 await 等待完成。
自訂快取儲存區
預設儲存區是每個伺服器執行個體各自的記憶體 LRU(1000 個項目,約 50 MB)。若要跨執行個體共用快取,請將 cacheHandler 指向預設匯出 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;項目採用純 JSON 格式。也可以使用 setCacheStore(store) 在執行階段安裝儲存區。
平台注意事項
- 每個無伺服器執行個體(Vercel functions、Cloudflare isolates)都有自己的記憶體內快取。
Cache-Control: s-maxage也能讓平台 CDN 快取回應;隨需重新驗證不會清除 CDN 快取。當revalidateTag必須涵蓋每個執行個體時,請使用共用的cacheHandler。 - Cloudflare 需要啟用
nodejs_compat旗標(快取使用AsyncLocalStorage)。請參閱部署。