# 个性化与缓存 · 每人不同的页面怎么缓存

> 「你好，张三」这种每人不同的页面，天生和共享缓存(CDN)水火不容——缓存了发给别人就是灾难。可我们又想要 CDN 的速度。这一章给出破局的整套工具：拆分静态壳与动态洞、按人群分段、边缘个性化、部分预渲染。这是缓存的压轴。

- 作者：David（道雾轩）
- 专栏：SSR 从入门到精通（https://daiw.org/manual/ssr.md）
- 最后更新：2026-10-01
- 原文：https://daiw.org/manual/ssr/personalization
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 个性化与缓存 · 每人不同的页面怎么缓存

缓存这一站，压轴的是最硬的一块骨头：**个性化内容**。

矛盾很尖锐。[HTTP 缓存那章](https://daiw.org/manual/ssr/http-caching) 划过一条红线：**个性化内容必须 `private`，绝不能进共享缓存**——否则 CDN 把「你好，张三」缓存下来发给李四，就是信息泄露。可另一边，我们又眼馋 [CDN 的速度和扛量](https://daiw.org/manual/ssr/cdn-caching)。**「每人不同」和「大家共享一份缓存」，天生对立。** 这一章讲怎么破。

## 破局的总纲：把「共享的」和「私有的」分开

所有解法都源自同一个洞见：

> **一个「个性化页面」，极少是 100% 个性化的。** 它通常是**一大片对所有人相同的内容**（布局、文章正文、商品信息、推荐榜）+ **一小撮每人不同的东西**（名字、头像、购物车数量、是否已登录）。

既然如此，就别把整页当成一个不可分的整体去纠结「能不能缓存」。**把共享的部分和私有的部分拆开，各用各的策略：共享的大片，尽情用 CDN 缓存；私有的一小撮，单独实时取。** 下面几种方案，都是这个总纲的不同实现。

```mermaid
flowchart TB
  subgraph Page["一个「个性化」页面"]
    S["共享外壳:布局 · 正文 · 榜单<br/>→ CDN 缓存(public),所有人共享,秒回"]
    H["个性化小洞:你好张三 · 购物车(3)<br/>→ 每请求 / 客户端单独取(private)"]
  end
```

## 方案一：静态壳 + 客户端补洞

最经典、最好落地：**把共享外壳做成可缓存的静态页（CDN 秒回），个性化的小洞留空，等页面到了浏览器再用一个客户端请求填上。**

- CDN 缓存并秒回那片共享外壳——首屏飞快、SEO 完整（正文都在）。
- 页面加载后，一个轻量的客户端请求(`/api/me`)拉回「张三、购物车 3 件」，填进预留的洞里。

代价：个性化的小洞有个「先空着、后填上」的短暂闪现。但**用极小的代价，换回了整页的 CDN 缓存**——绝大多数内容站的登录态导航栏就是这么做的。

## 方案二：按「人群」分段，而不是按人

有时你不想要客户端补洞的闪现，想让服务端**直接**发出带个性化的完整 HTML，又还能缓存。诀窍：**别按「每个用户」缓存（那是无穷多份，等于没缓存），按「人群段(segment)」缓存。**

<Callout type="info">
  **关键是缓存键的「基数(cardinality)」。** 按 `user_id` 做缓存键 → 一百万用户一百万份缓存，命中率趋零。但如果你的个性化其实只分**几类人群**——「登录 vs 未登录」「中国 vs 海外」「A/B 实验的 A 组 vs B 组」——那就按这个**低基数**的维度缓存：整个站可能只有「登录版」和「未登录版」两份缓存，依然高命中。用 [`Vary`](https://daiw.org/manual/ssr/http-caching) 或 CDN 的缓存键配置，按这个粗粒度维度分段即可。**个性化 ≠ 一定要精确到人；能降到「按段」，就还能缓存。**
</Callout>

典型：未登录访客全看同一份缓存页（占流量大头，缓存价值最高）；登录用户再走个性化路径。仅此一分，就把大部分流量挡在了缓存层。

## 方案三：边缘个性化

[边缘渲染那章](https://daiw.org/manual/ssr/edge-rendering) 埋过一个伏笔，这里收：**在边缘节点上，缓存共享外壳，再就地把个性化的小块注入进去。**

边缘节点离用户近、又能读到请求里的 **Cookie**，于是它可以：从边缘缓存取出共享外壳（~0 延迟）→ 从 Cookie 认出用户 / 人群 → 把「你好，张三」就地拼进去 → 发出。**共享部分吃缓存，个性化部分在离用户最近处轻量拼装**，兼得速度与个性化。这类「边缘拼装」的老祖宗是 ESI(Edge Side Includes)，如今被各家边缘中间件现代化地重新实现。

## 方案四：部分预渲染(PPR)

框架层最新的答案，是把「静态壳 + 动态洞」做成**一次请求内**的原生能力——**部分预渲染(Partial Prerendering,PPR)**:

- **构建时**就把共享外壳**预渲染成静态**（像 SSG，CDN 可缓存）。
- 页面里用 `<Suspense>` 圈出的个性化小洞，则在**每次请求时[流式](https://daiw.org/manual/ssr/streaming)地**填进那个静态壳里——**在同一个 HTTP 响应内**完成。

于是用户一个请求就拿到：**静态壳瞬间到达（缓存级速度），个性化的洞随后流式补上（实时）**，没有方案一那种「到浏览器再发第二个请求」的往返。这是「共享缓存」和「实时个性化」一次漂亮的融合，Next.js 等正在推进它落地。

## 一条必须焊死的纪律

不管用哪个方案，有一条铁律**违反即出事**：

<Callout type="warn">
  **绝不让任何一个用户的私有数据，污染共享缓存。** 具体到操作：① 会进共享缓存(`public`)的响应，**绝不能带 `Set-Cookie`**，也**绝不能**在 HTML 里嵌入任何用户私有数据；② 个性化响应一律 `private` 或 `no-store`；③ 拆分时想清楚每一块的「共享 / 私有」归属，别让个性化数据混进「共享外壳」里被缓存走。**[http 缓存那章](https://daiw.org/manual/ssr/http-caching) 说的「张三的页面被发给李四」，几乎都源于这条纪律的破防。** 个性化缓存的所有精巧，都建立在「共享的归共享、私有的归私有」这条界线绝不含糊之上。
</Callout>

## 小结与下一章

个性化和共享缓存的对立，靠一个总纲破解：**拆开「共享的大片」和「私有的一小撮」，各用各的策略。** 落地有四招——客户端补洞（最好上手）、按人群分段（降基数换命中）、边缘个性化（就近拼装）、部分预渲染（一次请求融合）。而贯穿始终的铁律是：**私有数据绝不能污染共享缓存。**

缓存这一整站（八章）到此讲完——从 HTTP 头到 CDN、SWR、ISR、数据缓存、失效、个性化。你已经有了让 SSR「扛得住」的整套武器。最后一站转向实战：这些东西上了生产，**该量什么指标、怎么排查**。👉 [量化与排查：TTFB、LCP 与缓存命中率](https://daiw.org/manual/ssr/measuring)。

## 参考文献

- [1] Next.js 文档.「Partial Prerendering.」https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering —— PPR 的静态壳 + 动态洞模型。
- [2] Fastly / Akamai.「Edge Side Includes (ESI) / edge personalization.」https://www.fastly.com/documentation/ —— 边缘拼装个性化。
- [3] web.dev.「Cache API + personalization patterns.」https://web.dev/articles/rendering-on-the-web —— 静态壳 + 客户端补洞、按段缓存。
