发布日期

第 11 讲|数据获取:fetch 增强、unstable_cache、'use cache'

fetch 增强机制、unstable_cache 与 use cache 指令的数据获取体系

阶段二:App Router 核心机制 · 第 11 / 40 讲 难度:⭐⭐⭐⭐ · 预计耗时:3.5 小时 配套 fixture:fixtures/lecture-11/

学习目标

  • 理解 fetch 在 Next.js 里被打了哪些"补丁":patchFetch / NextFetchRequestConfig / 自动 dedupe / 自动 tag 收集。
  • 看懂 unstable_cache 的工作流:fixedKey + invocationKey + IncrementalCache.get/set。
  • 掌握 'use cache' 指令的"分层缓存"模型:cacheLife / cacheTag / 隐式拉取 cookies。
  • 理解 CacheHandler 接口(5 个方法)与默认实现,能根据自定义后端实现 handler。
  • 在 fixture 中体验数据失效的三种触发:时间过期、revalidatePath / revalidateTag、Server Action 修改。

1. 三层缓存 API 全景

App Router 给开发者提供了三层数据缓存 API

API适用范围何时启用失效方式
fetch(url, { next: { revalidate, tags } })任何 server-side fetch 调用(含 SC、Route Handler)自动(patchFetch时间 / revalidateTag / revalidatePath
unstable_cache(fn, keyParts, { revalidate, tags })包裹任意纯函数(不一定是 fetch)显式时间 / revalidateTag
'use cache' 指令 + cacheLife / cacheTag整个函数 / 文件 / 整个组件树配合 cacheComponentscacheLife 决定 / cacheTag 收集

速记:fetch 适合"远程接口";unstable_cache 适合"本地计算或内部函数";'use cache' 适合"组件级"。

学完这一讲你会发现,三层都共享同一个底层基建:IncrementalCache + CacheHandler。理解这个底盘比记 API 名字重要得多。

2. fetch 增强:patchFetch

packages/next/src/server/lib/patch-fetch.ts 1300+ 行,但理解三个入口就够。

2.1 全局打补丁的"开关"

49:packages/next/src/server/lib/patch-fetch.ts
export const NEXT_PATCH_SYMBOL = Symbol.for('next-patch')

function isFetchPatched() {
  return (globalThis as Record<symbol, unknown>)[NEXT_PATCH_SYMBOL] === true
}

patchFetch 在 server bootstrap 阶段被调用一次,把 globalThis.fetch 替换为 createPatchedFetcher 返回的版本。NEXT_PATCH_SYMBOL 防止重复 patch(test 重启 server 时会重复 init,必须幂等)。

2.2 NextFetchRequestConfig 的字段

打补丁后 fetch(url, init) 多了一个 init.next 字段:

interface NextFetchRequestConfig {
  revalidate?: number | false; // 0 等价 no-store
  tags?: string[]; // 用于 revalidateTag
}
// 业务示例:商品详情接口
const res = await fetch(`https://api.example.com/products/${id}`, {
  next: {
    revalidate: 60, // 60 秒后台重新拉
    tags: [`product:${id}`, "product"], // 标签维度失效
  },
});

2.3 校验逻辑

79:packages/next/src/server/lib/patch-fetch.ts
export function validateRevalidate(
  revalidateVal: unknown,
  route: string
): undefined | number {
  try {
    let normalizedRevalidate: number | undefined = undefined

    if (revalidateVal === false) {
      normalizedRevalidate = INFINITE_CACHE
    } else if (
      typeof revalidateVal === 'number' &&
      !isNaN(revalidateVal) &&
      revalidateVal > -1
    ) {
      normalizedRevalidate = revalidateVal
    } else if (typeof revalidateVal !== 'undefined') {
      throw new Error(
        `Invalid revalidate value "${revalidateVal}" on "${route}", must be a non-negative number or false`
      )
    }
    return normalizedRevalidate
  } catch (err: any) {
    // ...
    return undefined
  }
}

要点:

  • falseINFINITE_CACHE(永久缓存,等价于"build 时静态化")。
  • 0 → 视为不缓存(动态)。
  • 必须 >=0

2.4 tag 校验

122:packages/next/src/server/lib/patch-fetch.ts
export function validateTags(tags: any[], description: string) {
  const validTags: string[] = []
  const invalidTags: Array<{
    tag: any
    reason: string
  }> = []

  for (let i = 0; i < tags.length; i++) {
    const tag = tags[i]

    if (typeof tag !== 'string') {
      invalidTags.push({ tag, reason: 'invalid type, must be a string' })
    } else if (tag.length > NEXT_CACHE_TAG_MAX_LENGTH) {
      invalidTags.push({
        tag,
        reason: `exceeded max length of ${NEXT_CACHE_TAG_MAX_LENGTH}`,
      })
    } else {
      // Encode so a non-ASCII tag can be safely serialized into the
      // `x-next-cache-tags` HTTP header without tripping Node's header
      // validation. Length is checked on the raw input above.
      validTags.push(encodeCacheTag(tag))
    }

    if (validTags.length > NEXT_CACHE_TAG_MAX_ITEMS) {
      console.warn(
        `Warning: exceeded max tag count for ${description}, dropped tags:`,
        tags.slice(i).join(', ')
      )
      break
    }
  }
  // ...
  return validTags
}

要点:

  • 长度上限 NEXT_CACHE_TAG_MAX_LENGTH(256);条数上限 NEXT_CACHE_TAG_MAX_ITEMS(128)。
  • 自动 encodeCacheTag(因为 tag 会落到 x-next-cache-tags HTTP 头里)。
  • 超长/非字符串只 warn,不报错——保证旧代码兼容。

2.5 cache: 'force-cache' | 'no-store'

除了 next.revalidate,原生 fetch 的 cache 字段也被识别:

  • force-cache → 等价 revalidate: false(永久缓存)。
  • no-store → 等价 revalidate: 0(不缓存)。

在 Next.js 14 之前,默认 fetch 行为是"force-cache";从 15 开始改为"no-store"。这是历史性默认值切换,老项目升级常踩。

2.6 自动 dedupe

同一次 server 渲染中,对同一个 URL 多次 fetch 只会执行一次——这是 React 的 cache(fn) 套了一层。例如:

async function getUser(id: string) {
  const res = await fetch(`/api/user/${id}`, { next: { revalidate: 60 } });
  return res.json();
}

// Page 与 Layout 同时调用
// 实际只发 1 次 HTTP 请求

dedupe 仅在单次请求生命周期内有效(AsyncLocalStorage 隔离),不同请求互不干扰。

2.7 fetchMetric 收集

打补丁的 fetch 还会把每次调用记进 workStore.fetchMetrics

139:packages/next/src/server/lib/patch-fetch.ts
function trackFetchMetric(
  workStore: WorkStore,
  ctx: Omit<FetchMetric, 'end' | 'idx'>
) {
  if (!workStore.shouldTrackFetchMetrics) {
    return
  }

  workStore.fetchMetrics ??= []

  workStore.fetchMetrics.push({
    ...ctx,
    end: performance.timeOrigin + performance.now(),
    idx: workStore.nextFetchId || 0,
  })
}

dev 模式 next dev 终端会打印每次 fetch 的耗时与 cache hit/miss,正是来自这里。生产里默认关闭。

生产排查提示:dev 跑得慢、Network 面板不直观时,看 dev server 终端的 fetch metric 日志,比浏览器端更准——它统计的是真正命中 server-side cache 的状态。

3. unstable_cache:函数级缓存

unstable_cache 是给"非 fetch"函数用的缓存包装。用例:

  • 读数据库 ORM 查询。
  • 走内部 SDK(不是 fetch)。
  • 任意昂贵的纯计算。

3.1 签名

71:packages/next/src/server/web/spec-extension/unstable-cache.ts
export function unstable_cache<T extends Callback>(
  cb: T,
  keyParts?: string[],
  options: {
    /**
     * The revalidation interval in seconds.
     */
    revalidate?: number | false
    tags?: string[]
  } = {}
): T {

3.2 fixedKey + invocationKey

135:packages/next/src/server/web/spec-extension/unstable-cache.ts
  // Stash the fixed part of the key at construction time. The invocation key will combine
  // the fixed key with the arguments when actually called
  // ...
  const fixedKey = `${cb.toString()}-${
    Array.isArray(keyParts) && keyParts.join(',')
  }`

  const cachedCb = async (...args: any[]) => {
    const workStore = workAsyncStorage.getStore()
    const workUnitStore = workUnitAsyncStorage.getStore()

    // We must be able to find the incremental cache otherwise we throw
    const maybeIncrementalCache:
      | import('../../lib/incremental-cache').IncrementalCache
      | undefined =
      workStore?.incrementalCache || (globalThis as any).__incrementalCache

    if (!maybeIncrementalCache) {
      throw new Error(
        `Invariant: incrementalCache missing in unstable_cache ${cb.toString()}`
      )
    }
    const incrementalCache = maybeIncrementalCache
    // ...
      // Construct the complete cache key for this function invocation
      const invocationKey = `${fixedKey}-${JSON.stringify(args)}`
      const cacheKey = await incrementalCache.generateCacheKey(invocationKey)

精髓:

  1. fixedKeyunstable_cache(cb) 调用时计算,包含 cb.toString() + keyParts.join(',')
  2. invocationKey 在每次调用时计算,加上 JSON.stringify(args)
  3. 最终通过 incrementalCache.generateCacheKey 加 hash 得到稳定 cacheKey。

注意 cb.toString() 的"潜规则"——修改函数体会自动失效缓存(key 变了)。这是非常方便的副作用:开发者改函数后无须手动清缓存。

3.3 keyParts 的必要场景

const getUser = unstable_cache(
  async (id: string) => db.user.findUnique({ where: { id } }),
  ["getUser"], // 推荐:避免不同 unstable_cache 的 closure 撞键
  { revalidate: 60, tags: ["users"] },
);

强烈建议传 keyParts——cb.toString() 看似稳定,但 minify / SWC transform 可能改变字符串内容;传明确的 keyParts 能减少误判。

3.4 不能在请求作用域内嵌套 fetch revalidate

unstable_cache 的执行上下文是 unstable-cache 类型 store,不是 request。所以包在 unstable_cache 里的 fetch 会丢失"动态"上下文,比如读不到 cookies。这个限制是有意的——避免缓存数据泄漏每个用户的 cookie。

4. 'use cache' 指令:组件级缓存

Next.js 15 引入的新指令,需要 experimental.cacheComponents 开启。它的语义是 React 已实验的 'use cache' 指令——在 Next.js 中接入到 IncrementalCache 体系。

4.1 三种使用形态

// 形态 A:函数级
async function getProducts() {
  "use cache";
  const res = await fetch("/api/products");
  return res.json();
}

// 形态 B:组件级
async function ProductList() {
  "use cache";
  const products = await db.product.findMany();
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

// 形态 C:文件级(顶部加指令)
("use cache");
export async function getProducts() {
  /* ... */
}

4.2 cacheLife:定义缓存窗口

15:packages/next/src/server/use-cache/cache-life.ts
export type CacheLife = {
  // How long the client can cache a value without checking with the server.
  stale?: number
  // How frequently you want the cache to refresh on the server.
  // Stale values may be served while revalidating.
  revalidate?: number
  // In the worst case scenario, where you haven't had traffic in a while,
  // how stale can a value be until you prefer deopting to dynamic.
  // Must be longer than revalidate.
  expire?: number
}

注释说得很清楚——它就是把 HTTP Cache-Control 三参数模型搬到代码里:

  • stale:客户端能用这个值多久而不回服务器。
  • revalidate:服务器多久后台重新拉。
  • expire:最坏情况下值还能用多久(必须大于 revalidate)。

4.3 cacheLife 内置 profile

32:packages/next/src/server/use-cache/cache-life.ts
type CacheLifeProfiles =
  | 'default'
  | 'seconds'
  | 'minutes'
  | 'hours'
  | 'days'
  | 'weeks'
  | 'max'
  | (string & {})

7 个内置档:default seconds minutes hours days weeks max。开发者可以在 next.config.js 自定义新 profile:

// next.config.js
module.exports = {
  experimental: {
    cacheLife: {
      // 自定义 5 分钟 profile
      "five-minutes": {
        stale: 60,
        revalidate: 300,
        expire: 600,
      },
    },
  },
};
async function getX() {
  "use cache";
  cacheLife("five-minutes");
  // ...
}

4.4 cacheTag:标签收集

41:packages/next/src/server/use-cache/cache-tag.ts
export function cacheTag(...tags: string[]): void {
  if (!process.env.__NEXT_USE_CACHE) {
    throw new Error(
      '`cacheTag()` is only available with the `cacheComponents` config.'
    )
  }

  const workUnitStore = workUnitAsyncStorage.getStore()

  switch (workUnitStore?.type) {
    case 'prerender':
    case 'prerender-client':
    case 'validation-client':
    case 'prerender-runtime':
    case 'prerender-ppr':
    case 'prerender-legacy':
    case 'request':
    case 'unstable-cache':
    case 'generate-static-params':
    case undefined:
      throw new Error(
        '`cacheTag()` can only be called inside a "use cache" function.'
      )
    case 'cache':
    case 'private-cache':
      break
    default:
      workUnitStore satisfies never
  }

  const validTags = validateTags(tags, '`cacheTag()`')

  if (!workUnitStore.tags) {
    workUnitStore.tags = validTags
  } else {
    workUnitStore.tags.push(...validTags)
  }
}

要点:

  • 只能在 'cache''private-cache' workUnit 内调用——其它 store 都报错。
  • 多次调用会累加到当前 cache scope 的 tags。
  • 调用 revalidateTag('xxx') 时会让所有标了 xxx 的 cache scope 全部失效。

4.5 cacheLife 与 cacheTag 的协同

175:packages/next/src/server/use-cache/cache-life.ts
  if (profile.revalidate !== undefined) {
    // Track the explicit revalidate time.
    if (
      workUnitStore.explicitRevalidate === undefined ||
      workUnitStore.explicitRevalidate > profile.revalidate
    ) {
      workUnitStore.explicitRevalidate = profile.revalidate
    }
  }
  if (profile.expire !== undefined) {
    // Track the explicit expire time.
    if (
      workUnitStore.explicitExpire === undefined ||
      workUnitStore.explicitExpire > profile.expire
    ) {
      workUnitStore.explicitExpire = profile.expire
    }
  }
  if (profile.stale !== undefined) {
    // Track the explicit stale time.
    if (
      workUnitStore.explicitStale === undefined ||
      workUnitStore.explicitStale > profile.stale
    ) {
      workUnitStore.explicitStale = profile.stale
    }
  }

注意 > 判断——取所有 cacheLife 中的最小值。这是因为缓存的 outer scope 持有 inner scope,inner 的 stale 必须不大于 outer。

5. CacheHandler:可插拔的缓存后端

packages/next/src/server/lib/cache-handlers/types.ts 定义了 5 个方法接口:

80:packages/next/src/server/lib/cache-handlers/types.ts
export interface CacheHandler {
  /**
   * Retrieve a cache entry for the given cache key, if available. Will return
   * undefined if there's no valid entry, or if the given soft tags are stale.
   */
  get(cacheKey: string, softTags: string[]): Promise<undefined | CacheEntry>

  /**
   * Store a cache entry for the given cache key. When this is called, the entry
   * may still be pending, i.e. its value stream may still be written to. So it
   * needs to be awaited first. If a `get` for the same cache key is called,
   * before the pending entry is complete, the cache handler must wait for the
   * `set` operation to finish, before returning the entry, instead of returning
   * undefined.
   */
  set(cacheKey: string, pendingEntry: Promise<CacheEntry>): Promise<void>

  /**
   * This function may be called periodically, but always before starting a new
   * request. If applicable, it should communicate with the tags service to
   * refresh the local tags manifest accordingly.
   */
  refreshTags(): Promise<void>

  /**
   * This function is called for each set of soft tags that are relevant at the
   * start of a request. The result is the maximum timestamp of a revalidate
   * event for the tags. Returns `0` if none of the tags were ever revalidated.
   * Returns `Infinity` if the soft tags are supposed to be passed into the
   * `get` method instead to be checked for expiration.
   */
  getExpiration(tags: string[]): Promise<Timestamp>

  /**
   * This function is called when tags are revalidated/expired. If applicable,
   * it should update the tags manifest accordingly.
   */
  updateTags(tags: string[], durations?: { expire?: number }): Promise<void>
}

5.1 CacheEntry 形态

40:packages/next/src/server/lib/cache-handlers/types.ts
export interface CacheEntry {
  /**
   * The ReadableStream can error and only have partial data so any cache
   * handlers need to handle this case and decide to keep the partial cache
   * around or not.
   */
  value: ReadableStream<Uint8Array>

  /**
   * The tags configured for the entry excluding soft tags
   */
  tags: string[]

  /**
   * This is for the client, not used to calculate cache entry expiration
   * [duration in seconds]
   */
  stale: number

  /**
   * When the cache entry was created [timestamp in milliseconds]
   */
  timestamp: Timestamp

  /**
   * How long the entry is allowed to be used (should be longer than revalidate)
   * [duration in seconds]
   */
  expire: number

  /**
   * How long until the entry should be revalidated [duration in seconds]
   */
  revalidate: number
}

注意 valueReadableStream<Uint8Array> 而不是 string —— 因为 'use cache' 缓存的是 RSC payload 流,必须支持 partial / 错误中断。

5.2 默认 handler:内存 + 磁盘

cache-handlers/default.ts 的实现思路:

  1. 内存 LRU:常用 entry 存内存(按字节数限)。
  2. 磁盘 fallback:超出 LRU 的写到 .next/cache/...
  3. tags manifest:JSON 文件维护 tag → revalidate timestamp 映射。
  4. 进程内单例(每个 worker 一份)。

5.3 自定义 handler:典型案例

业务里常见自定义场景:

  • 多副本部署next start 起多份,需要共享 cache → Redis handler。
  • CDN 一体化:把 entry 直接发到 Cloudflare KV / Vercel Edge → 适配器 handler。
  • 审计与脱敏:缓存写入前做敏感字段过滤。

模板代码:

// cache-handler.ts
import type {
  CacheHandler,
  CacheEntry,
} from "next/dist/server/lib/cache-handlers/types";

export default class RedisCacheHandler implements CacheHandler {
  async get(key: string, softTags: string[]) {
    // 从 Redis 读 entry,检查 softTags
    return undefined;
  }
  async set(key: string, pendingEntry: Promise<CacheEntry>) {
    const entry = await pendingEntry;
    // 把 entry.value 流读完写到 Redis
  }
  async refreshTags() {
    /* ... */
  }
  async getExpiration(tags: string[]) {
    return 0;
  }
  async updateTags(tags: string[]) {
    /* ... */
  }
}
// next.config.js
module.exports = {
  experimental: {
    cacheHandler: require.resolve("./cache-handler.ts"),
  },
};

生产排查提示:怀疑 cache handler 行为?在每个方法加 console.log 把 cacheKey / tags 打出来;dev 模式下立刻能看。

6. revalidatePath / revalidateTag:失效入口

next/cache 暴露两个 API:

  • revalidatePath(path, type?):让某个 URL 路径下所有 cache 失效。
  • revalidateTag(tag):让带某个 tag 的所有 cache 失效。

它们的实现都是在当前请求的 workStore 上记一个 "pending revalidate",请求结束时 flush 给 cacheHandler 的 updateTags

6.1 业务案例:电商后台修改商品

// app/admin/products/[id]/edit/page.tsx
"use server";

export async function updateProduct(id: string, data: FormData) {
  await db.product.update({ where: { id }, data: parse(data) });

  // 失效 detail 与 list
  revalidateTag(`product:${id}`);
  revalidateTag("product:list");

  // 也可以按路径
  revalidatePath("/admin/products");
}

当下次 fetch('/api/products', { next: { tags: ['product:list'] } }) 被调用时,getExpiration(['product:list']) 返回的 timestamp 大于 entry timestamp,cache miss → 重新拉。

6.2 Soft tag vs Hard tag 区别

  • hard tag:写入 entry 时随 entry 持久化(CacheEntry.tags)。
  • soft tag:在 get(cacheKey, softTags) 时由调用方传入——典型如 layout 共享路径前缀。

soft tag 的好处:不必为每个 entry 重新写入即可让 layout 范围的失效生效。

7. 业务案例:商品列表 + 详情 + 后台编辑

// 1. 列表页:fetch + tag
async function getProducts() {
  "use cache";
  cacheLife("hours");
  cacheTag("product:list");

  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 3600, tags: ["product:list"] },
  });
  return res.json() as Promise<Product[]>;
}

// 2. 详情页:fetch + per-id tag
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 60, tags: [`product:${id}`, "product:list"] },
  });
  return res.json() as Promise<Product>;
}

// 3. ORM 调用:unstable_cache
const getProductCount = unstable_cache(
  async () => db.product.count(),
  ["getProductCount"],
  { revalidate: 300, tags: ["product:list"] },
);

// 4. server action 修改商品
async function deleteProduct(id: string) {
  "use server";
  await db.product.delete({ where: { id } });
  revalidateTag(`product:${id}`);
  revalidateTag("product:list");
}

观察这套设计:

  • 三层 API 共用同一个 tag 命名空间——'product:list' 同时被 fetch / 'use cache' / unstable_cache 持有。
  • 一次 revalidateTag 调用同时打掉这三个 entry。
  • 默认 stale-while-revalidate:用户先看到老数据,后台静默更新——零等待。

8. 重难点

8.1 fetch 的 cache 字段与 next.revalidate 的优先级

如果同时给 cache: 'force-cache'next: { revalidate: 60 }next.revalidate 优先(因为它更精确)。但建议只用一种避免歧义。

8.2 unstable_cache 不能调用 cookies()

unstable_cache 创建独立的 unstable-cache workUnit,与 request 隔离——无法读 cookies / headers。如果你需要"按用户缓存",应该把用户 ID 当 keyParts:

// 错的:cookies() 在 unstable_cache 里抛错
const getUserData = unstable_cache(async () => {
  const c = cookies(); // throws
});

// 对的:用户 ID 当 keyParts
const getUserData = unstable_cache(
  async (userId: string) => fetchUserData(userId),
  ["getUserData"],
  { revalidate: 60 },
);

8.3 'use cache' 与 cookies() 互斥

'use cache' 函数视为可缓存——内部不能调用 cookies() headers() searchParams,否则视为"动态 escape",编译期就会报错。要在 cache scope 中读 cookies,请改用 'use private cache'(仅 PPR 启用 cacheComponents 时)。

8.4 revalidate vs expire vs stale

记忆口诀:

  • revalidate:触发后台刷新。
  • expire:到点必须丢弃。
  • stale:客户端不用问服务器的舒适窗口。

expire >= revalidate 是硬性约束——前面已经看到代码强校验。

8.5 测试缓存行为的方法

  • 用 dev mode 时,重启 server 会清空内存 cache(不会清磁盘)。
  • 用 prod 模式:pnpm build && pnpm start,此时缓存全部走磁盘 + LRU。
  • 想强制清空:rm -rf .next/cache

9. 配套 fixture:动手观察三层缓存

fixtures/lecture-11/ 含 4 个示例:

  1. app/example-1-fetch/ — 不同 next.revalidate 取值的对比。
  2. app/example-2-unstable-cache/ — 函数级缓存与 keyParts。
  3. app/example-3-use-cache/'use cache' + cacheLife + cacheTag(需开启 cacheComponents)。
  4. app/example-4-revalidate/ — 触发 revalidatePath / revalidateTag 失效。

9.1 推荐实验

cd learning/nextjs-40-lectures/fixtures/lecture-11
pnpm install --ignore-workspace
pnpm dev
# 浏览器打开 http://localhost:3011/

实验 A:fetch revalidate 对比

  • /example-1,3 张卡片分别用 revalidate: 0 / 60 / false
  • 第一次访问都要拉 API;第二次访问只有 0 重新拉。
  • dev 终端打印 fetch metric 验证。

实验 B:unstable_cache 命中

  • /example-2 计算斐波那契——首次慢,二次秒回。
  • keyParts 重新部署,验证缓存独立。

实验 C:'use cache' 与 cacheLife

  • /example-3 显示一个商品列表('use cache' + cacheLife('seconds'))。
  • 服务端 console 打印 "regenerating list"——只有缓存过期时才出现。
  • next.config.jsexperimental.cacheComponents: true

实验 D:revalidate 触发失效

  • /example-4 含 "Refresh products" 按钮,调用 revalidateTag('product:list')
  • 点击后再访问 /example-1/example-3,看到列表 cache miss,重新生成。

10. 检验问题

  1. fetch 在 server 端被 patch 后,init.next 增加了哪些字段?分别约束什么?
  2. validateRevalidate 的 3 个分支与 INFINITE_CACHE 的关系?
  3. unstable_cache 的 fixedKey 与 invocationKey 各包含什么?为什么强烈建议传 keyParts?
  4. 'use cache'unstable_cache 各自的"上下文限制"是什么?读 cookies 会发生什么?
  5. CacheHandler 的 5 个方法各自承担什么职责?getsoftTags 参数怎么理解?
  6. cacheLifestale revalidate expire 的语义?为什么 expire >= revalidate
  7. 自定义 cacheHandler 的接入方式?最常见的业务场景是什么?
  8. revalidatePath('/products') 的失效粒度?与 revalidateTag('product:list') 哪个优先?
  9. 为什么 unstable_cache 内嵌 fetch 也得显式传 keyParts?
  10. fixture 实验 D 中点击 Refresh 后,三张 example 哪些会立即 miss?为什么?

11. 延伸阅读

  • 源码:packages/next/src/server/lib/patch-fetch.ts
  • 源码:packages/next/src/server/web/spec-extension/unstable-cache.ts
  • 源码:packages/next/src/server/use-cache/{cache-life,cache-tag,use-cache-wrapper}.ts
  • 源码:packages/next/src/server/lib/cache-handlers/{types,default}.ts
  • 文档:docs/01-app/02-guides/incremental-static-regeneration.mdx
  • 配套 fixture:fixtures/lecture-11/

下一讲预告

第 12 讲|Server Actions、Form 与 mutation:本讲讲数据"读"的缓存;下一讲讲数据"写"——Server Actions 的编译产物、encryptionKey、actionId 路由、useFormState / useActionState 流式状态。会展示一个真实的"商品 CRUD"案例。