Skip to content
签到签到

第 7 章:C 端产品、体验与渲染架构

本章定位

C 端产品关注首屏、交互、渲染架构和失败恢复。AI 对话产品特别需要流式体验、乐观更新、断线重连和会话恢复,本章把这些能力统一到状态和渲染模型中。

必须掌握清单

必须掌握

  • 能解释 CSR、SSR、SSG、ISR 和 Islands 的取舍。

    相关答案: CSR 在浏览器生成主要内容,交互简单但首屏依赖脚本;SSR 每次请求生成 HTML;SSG 构建时生成;ISR 在静态结果上按策略更新;Islands 只让局部区域水合。选型依据是内容变化频率、SEO、个性化、缓存和服务端成本。

    具体例子: 营销页可用 SSG,新闻详情用 ISR,强个性化账户页用 SSR 或 CSR;文章主体静态输出、评论框作为 Island,可减少不必要的客户端 JavaScript。

  • 能处理 hydration 不匹配。

    相关答案: hydration 要求客户端首次渲染结果与服务端 HTML 一致。时间、随机数、浏览器专属 API、不同数据快照和非法 HTML 嵌套都会造成不匹配,应把非确定性逻辑延后到挂载后或通过服务端数据显式传递。

    具体例子: 服务端和客户端分别调用 Date.now() 会生成不同文本;应由服务端输出固定时间戳,客户端首次使用同一值,挂载后再按用户时区格式化。

  • 能设计乐观更新和失败回滚。

    相关答案: 乐观更新先保存旧状态或生成可逆补丁,再立即展示预期结果;请求失败时回滚或补偿,成功时用服务端权威数据校正。还要处理重复提交、并发响应乱序和临时 ID。

    具体例子: 点赞后本地计数立即加一并禁用重复请求;服务端拒绝时恢复旧计数并提示,成功时采用响应中的最终计数,避免多个客户端更新造成偏差。

  • 能避免高频 token 触发整棵树重渲染。

    相关答案: 将流式文本状态放入独立 store 或消息级状态,按帧合批生成不可变快照,并让组件只订阅所需切片。仅使用 memo 不能解决根节点状态每个 chunk 都变化的问题。

    具体例子: token 先进入当前消息缓冲区,每帧发布一次快照;正在生成的消息组件更新,历史消息、输入框和会话侧边栏保持稳定。

  • 能设计断线重连后的状态恢复。

    相关答案: 客户端需要保存会话 ID、最后确认的事件序号和请求状态;重连时从服务端按序号补取缺失事件,再以幂等方式合并。不能仅重新发送原请求,否则可能重复执行写操作或工具调用。

    具体例子: SSE 断开前已消费到事件 42,重连携带 Last-Event-ID: 42,服务端从 43 开始回放;客户端按事件 ID 去重,再继续接收实时 token。

原理讲解

SSR 能改善首屏和 SEO,但会带来服务端成本和 hydration。CSR 更简单,但首屏依赖脚本。Islands 把页面拆成可独立交互的区域,减少不必要的 hydration。

乐观更新先应用预期结果,再等服务端确认。失败时需要回滚或补偿。对于 AI 工具调用,界面可以先显示执行中,再根据工具结果进入成功、失败或取消状态。

高频 token 更新应该进入一个外部 store,React 通过 useSyncExternalStore 订阅快照,而不是每个 chunk 调用一次 setState

代码示例或模板

ts
type ChatSnapshot = {
  text: string;
  status: "streaming" | "done" | "error";
};

let snapshot: ChatSnapshot = { text: "", status: "streaming" };
const listeners = new Set<() => void>();

function appendToken(token: string) {
  snapshot = { ...snapshot, text: snapshot.text + token };
  listeners.forEach((listener) => listener());
}

function subscribe(listener: () => void) {
  listeners.add(listener);
  return () => listeners.delete(listener);
}

function getSnapshot() {
  return snapshot;
}

这是 React-free store 的最小形态,React 端用 useSyncExternalStore(subscribe, getSnapshot) 读取不可变快照。

AI Agent 概念对照

  • 流式 Markdown 对应 token 增量渲染。
  • 乐观 UI 对应工具 pending 状态。
  • 断线重连对应会话恢复。
  • 外部 store 对应 React-free object layer。

面试追问

1. 一个内容型页面为什么不一定需要 SSR?

查看深度解析

标准回答: 内容型页面需要的是可抓取、快首屏和可缓存,不一定要求每次请求动态 SSR。内容更新频率低且个性化少时,SSG 或 ISR 能直接输出 HTML,并用 CDN 获得更低成本和更稳定延迟。

原理展开: SSR 的价值在请求时个性化和实时组装,但会增加服务器计算、缓存失效和 hydration 复杂度;选型应基于数据时效、SEO、用户差异和发布频率。

工程示例: 帮助中心文章构建时生成,内容更新触发增量重建;登录后的推荐区在客户端加载,无需让整页 SSR。

常见误区: 把 SEO 与 SSR 绑定,忽略静态 HTML 同样可抓取;或为了“现代架构”让稳定内容每次请求都计算。

继续追问: 内容要求一分钟内更新怎么办?

回答方向: 评估按需 revalidate、短 TTL CDN 或局部服务端数据;只有请求时强个性化才必然倾向 SSR。

2. Islands 架构如何减少 hydration 成本?

查看深度解析

标准回答: Islands 默认把页面主体作为静态 HTML,只对需要交互的局部组件发送 JavaScript 和执行 hydration,因此减少下载、解析、执行及整树绑定事件的成本。

原理展开: 每个 Island 有明确的 props 序列化和激活策略,可按可见、空闲或用户交互延迟加载;岛之间不应依赖隐式共享组件状态。

工程示例: 商品详情文字和图片静态输出,购买按钮与评价筛选是两个 Island,后者滚入视口才加载。

常见误区: 把页面拆成很多 Island 却每个都加载同一大型运行时,或岛之间频繁跨边界同步,收益会被抵消。

继续追问: 多个 Island 如何共享购物车?

回答方向: 使用小型外部 store、浏览器事件或服务端状态作为显式协议,控制一致性;不要恢复成隐藏的整页组件树。

3. 乐观更新如何设计撤销和补偿?

查看深度解析

标准回答: 发请求前保存旧状态或生成可逆 patch,立即应用带 request ID 的乐观状态;失败时只撤销该请求仍拥有的变化,成功时用服务端权威结果校正。不可逆副作用需要补偿动作而非简单回滚 UI。

原理展开: 并发请求会导致响应乱序,不能恢复整份旧快照覆盖其他成功修改;还要区分网络未知、业务拒绝和用户取消。

工程示例: 编辑评论时记录字段级 patch,第二次编辑已发生后,第一次失败只撤销自己影响的版本;创建记录使用临时 ID,成功后原子替换。

常见误区: 请求失败就把整个列表恢复到提交前,抹掉别的用户操作;或把支付等不可逆行为做自动乐观成功。

继续追问: 离线状态怎么做?

回答方向: 把操作写入持久化 outbox,附幂等键和依赖顺序;联网重放时展示冲突,不能假设全部自动合并。

4. 如何把 SSE 内容渲染和 React 更新解耦?

查看深度解析

标准回答: 独立运行时负责解析 SSE、维护消息缓冲和合批,React 通过外部 store 的不可变快照订阅所需切片。网络事件不直接调用页面顶层 setState

原理展开: 运行时和 UI 使用不同频率:前者可接收每个 token,后者按帧发布;useSyncExternalStore 能让并发渲染读取一致快照。

工程示例: text_delta 写入当前 message buffer,rAF 时递增 snapshot version;只有 MessageRow(id) 订阅对应消息文本。

常见误区: store 每次仍复制整个会话并通知所有订阅者,形式上解耦但渲染成本没有下降。

继续追问: 完成和错误事件也需要等下一帧吗?

回答方向: 通常立即 flush,保证按钮、状态与资源清理及时;纯文本 delta 才适合合批。

5. 断线重连后如何恢复未完成的消息?

查看深度解析

标准回答: 客户端保存 session/request ID 和最后确认事件序号,重连时先查询请求权威状态,再从该序号补取事件并按 ID 去重。不能简单重新发送原 prompt,否则可能重复生成或执行工具。

原理展开: 恢复依赖服务端可续传日志或短期 replay buffer;状态机要区分仍运行、已完成、失败和不可恢复过期。

工程示例: 已消费到 event 42,重连携带 lastEventId,服务端回放 43 以后事件;完成后再切回实时流。

常见误区: 把当前已显示文本作为游标,重复 token 无法可靠去重;或重连无限重试却不展示离线状态。

继续追问: 服务端没有事件回放能力怎么办?

回答方向: 至少提供按 request ID 查询最终快照;流中断期间显示未知状态,不能伪造连续 token,必要时让用户明确重试新请求。

实践任务

实现一个带流式输出、乐观工具状态和断线重连的聊天页面,使用外部 store 订阅消息快照。

验收标准

  • token 更新不会每个 chunk 都触发整棵树渲染。
  • 工具失败后能回到可解释的错误状态。
  • 能说明重连后的恢复策略。

关联材料