重新验证

缓存爽快,但也要能「让它过期」。Next.js 提供两条路线:基于时间cacheLife,以及 按需revalidateTag / updateTag / revalidatePath。本章讲清楚它们的区别与使用场景。

时间维度:cacheLife

cacheLifeuse cache 作用域里指定缓存寿命:

import { cacheLife } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheLife('hours');
  return db.query('SELECT * FROM products');
}

内置档位表:

profile stale revalidate expire
seconds 0 1s 60s
minutes 5m 1m 1h
hours 5m 1h 1d
days 5m 1d 1w
weeks 5m 1w 30d
max 5m 30d ~永久

也可以传对象自定义:

cacheLife({
  stale: 3600,       // 1 小时后视为过时
  revalidate: 7200,  // 2 小时后重新生成
  expire: 86400,     // 1 天后彻底失效
});

Tag 维度:cacheTag

给一份缓存打标签,之后按标签批量失效:

import { cacheTag } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheTag('products');
  return db.query('SELECT * FROM products');
}

revalidateTag:后台刷新(SWR)

适合「晚一点刷新也没问题」的场景(博客、商品目录)。stale-while-revalidate 语义:先把旧数据发出去,后台重新生成。

import { revalidateTag } from 'next/cache';

export async function updateUser() {
  // 写数据…
  revalidateTag('user', 'max'); // 'max' 允许最长的 stale 窗口
}

可在 Server Action 或 Route Handler 中调用。

updateTag:立即失效(读自己写)

适合用户「立刻看到自己刚写的东西」的场景:

import { updateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: { title: formData.get('title') },
  });

  updateTag('posts');
  redirect(`/posts/${post.id}`);
}

只能在 Server Action 里用。

二者对比

updateTag revalidateTag
使用场所 仅 Server Action Server Action + Route Handler
失效行为 立即过期 stale-while-revalidate
典型场景 读自己写 后台刷新

revalidatePath:按路径失效

不确定要失效哪些 tag 时的粗粒度方案:

import { revalidatePath } from 'next/cache';

export async function updateUser() {
  // 写数据…
  revalidatePath('/profile');
}

建议:优先使用 tag 失效,精准且不会「误伤」其他缓存。

缓存什么?

做一个简单的判断:

  • 不依赖运行时数据(cookies / headers / searchParams)且可以容忍一段时间旧?→ 缓存
  • 每次请求都必须最新?→ 不缓存,配 <Suspense> 流式输出。
  • CMS 内容?→ 用 tag + 长 cacheLife,发布后 revalidateTag 触发刷新。

重新驗證

有快取就要能「令其過期」。Next.js 提供兩種策略:基於時間cacheLife,以及 按需revalidateTag / updateTag / revalidatePath。本章講清楚它們的差異與使用時機。

時間維度:cacheLife

cacheLifeuse cache 範圍內設定快取壽命:

import { cacheLife } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheLife('hours');
  return db.query('SELECT * FROM products');
}

內建檔位表:

profile stale revalidate expire
seconds 0 1s 60s
minutes 5m 1m 1h
hours 5m 1h 1d
days 5m 1d 1w
weeks 5m 1w 30d
max 5m 30d ~永久

也可以傳入物件自訂:

cacheLife({
  stale: 3600,       // 1 小時後視為過時
  revalidate: 7200,  // 2 小時後重新生成
  expire: 86400,     // 1 天後完全失效
});

Tag 維度:cacheTag

替一份快取加上標籤,之後按標籤批次失效:

import { cacheTag } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheTag('products');
  return db.query('SELECT * FROM products');
}

revalidateTag:背景刷新(SWR)

適合「延遲刷新可接受」的場景(網誌、商品目錄)。stale-while-revalidate 語意:先送舊資料,背景再重新生成。

import { revalidateTag } from 'next/cache';

export async function updateUser() {
  // 寫資料…
  revalidateTag('user', 'max'); // 'max' 允許最長的 stale 窗口
}

可在 Server Action 或 Route Handler 中呼叫。

updateTag:立即失效(讀自己寫)

適合使用者「立刻看到自己剛寫的內容」場景:

import { updateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: { title: formData.get('title') },
  });

  updateTag('posts');
  redirect(`/posts/${post.id}`);
}

只能在 Server Action 中使用。

兩者比較

updateTag revalidateTag
使用場所 僅 Server Action Server Action + Route Handler
失效行為 立即過期 stale-while-revalidate
典型場景 讀自己寫 背景刷新

revalidatePath:按路徑失效

不確定要失效哪些 tag 時的粗粒度方案:

import { revalidatePath } from 'next/cache';

export async function updateUser() {
  // 寫資料…
  revalidatePath('/profile');
}

建議:優先使用 tag 失效,精準且不會「誤傷」其他快取。

應該快取什麼?

簡單判斷:

  • 不依賴執行時資料(cookies / headers / searchParams),且可容忍一段時間舊?→ 快取
  • 每次請求都須最新?→ 不快取,搭配 <Suspense> 串流輸出。
  • CMS 內容?→ 用 tag + 長 cacheLife,發佈後以 revalidateTag 觸發刷新。

Revalidating

Caching is great — expiring caches is what makes it safe. Next.js offers two strategies: time-based with cacheLife, and on-demand with revalidateTag / updateTag / revalidatePath. This chapter covers both.

Time-based: cacheLife

cacheLife sets the cache lifetime inside a use cache scope:

import { cacheLife } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheLife('hours');
  return db.query('SELECT * FROM products');
}

Built-in profiles:

profile stale revalidate expire
seconds 0 1s 60s
minutes 5m 1m 1h
hours 5m 1h 1d
days 5m 1d 1w
weeks 5m 1w 30d
max 5m 30d ~indefinite

Or pass a custom object:

cacheLife({
  stale: 3600,       // 1 hour before considered stale
  revalidate: 7200,  // 2 hours before regenerated
  expire: 86400,     // 1 day before fully expired
});

Tag-based: cacheTag

Tag cached work so you can invalidate it in batches later:

import { cacheTag } from 'next/cache';

export async function getProducts() {
  'use cache';
  cacheTag('products');
  return db.query('SELECT * FROM products');
}

revalidateTag: background refresh (SWR)

Best for "a slight delay is OK" — blogs, product catalogs. Stale-while-revalidate: serve the old version, regenerate in the background.

import { revalidateTag } from 'next/cache';

export async function updateUser() {
  // mutate…
  revalidateTag('user', 'max'); // 'max' = longest stale window
}

Can be called from a Server Action or a Route Handler.

updateTag: immediate invalidation (read-your-own-writes)

Best when a user must see their own change immediately:

import { updateTag } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const post = await db.post.create({
    data: { title: formData.get('title') },
  });

  updateTag('posts');
  redirect(`/posts/${post.id}`);
}

Only available inside Server Actions.

Side by side

updateTag revalidateTag
Where Server Actions only Server Actions + Route Handlers
Behavior Expires immediately Stale-while-revalidate
Typical use Read-your-own-writes Background refresh

revalidatePath: coarse by URL

Invalidate all cached work tied to a route when you're not sure which tags apply:

import { revalidatePath } from 'next/cache';

export async function updateUser() {
  // mutate…
  revalidatePath('/profile');
}

Prefer tag-based invalidation when possible — it's precise and avoids over-invalidating.

What should I cache?

Quick heuristic:

  • Doesn't depend on runtime data (cookies / headers / searchParams) and can tolerate some staleness? → Cache it.
  • Must be fresh on every request? → Don't cache; use <Suspense> to stream.
  • CMS content? → Use tags + long cacheLife, and trigger revalidateTag when content is published.