快取與重新驗證

路由區段設定、unstable_cache、revalidateTag / revalidatePath 與 stale-while-revalidate。

快取路由處理常式的回應與資料,再定時或依需求重新驗證。執行一次背景更新時,立即提供過期的項目。

路由處理常式

GET 路由處理常式預設為動態。使用路由區段設定啟用快取:

app/api/posts/route.tsts
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-cacheMISS、HIT 或 STALE
Cache-Controls-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 快取任何非同步函式:

lib/posts.tsts
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 時:

  1. 下一個請求會立即收到過期值(STALE)。
  2. 背景執行一次更新;並行請求共用這次更新。
  3. 如果更新擲回錯誤,會繼續提供過期值並記錄錯誤。

在 Vercel 和 Cloudflare 上,背景更新會透過 waitUntil() 註冊。超過一年的項目一律在前景重新計算。

依需求重新驗證

app/api/revalidate/route.tsts
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 的模組:

text.config.tsts
export default defineConfig({ cacheHandler: "./cache-handler.ts" });
cache-handler.tsts
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)。請參閱部署。
在 GitHub 編輯此頁