主题
React + AI 应用开发
AI 流式消息如何设计增量状态更新?
高级实现要让高频 token 更新不拖垮 React,同时保证消息顺序、会话隔离和最终一致。
面试官想考什么
- streaming 消息和普通消息是否用同一种状态? 考察状态建模。
- 如何减少 token 级重渲染? 考察 React 性能判断。
- 切换会话时旧流还在返回怎么办? 考察竞态和生命周期。
- 为什么
memo有时仍然无效? 考察引用稳定性与组件边界。
一句话回答
text
我会把消息状态建模为按会话和 messageId 索引的状态机,流回调只写入增量 buffer,再按时间窗口批量提交;通过 requestId、AbortController 和版本检查丢弃过期事件,重组件用 memo 或拆分订阅降低渲染范围。面试回答详解
1. 状态分层
- 传输状态:reader、连接状态、buffer、最后 cursor。
- 消息状态:queued、streaming、completed、failed、cancelled。
- 视图状态:是否滚动到底部、是否展开引用、是否显示工具详情。
- 服务端状态:已落库的消息、usage、模型版本和错误码。
不要用一个 isLoading 覆盖这些状态,否则停止、重试和多个会话会互相污染。
2. 推荐的数据形状
ts
type Message = {
id: string
conversationId: string
role: "user" | "assistant" | "tool"
text: string
status: "queued" | "streaming" | "completed" | "failed" | "cancelled"
revision: number
}消息列表可按 id 存储、按顺序存 id。delta 更新只改目标消息,避免复制整个大型对象树。若使用 Context,列表项最好通过 selector 或拆分的外部 store 订阅,避免任意 token 让所有消息重渲染。
3. 批量提交策略
reader 回调把 delta 放入 buffer,并由 requestAnimationFrame、定时器或固定字符数触发 flush。flush 时使用 reducer 的函数式更新。对于代码高亮、Markdown AST、自动滚动等重操作,生成中使用轻量路径,完成后再完整计算。
4. React 并发能力
useTransition适合把非紧急的列表、搜索结果或格式化结果标成低优先级。useDeferredValue可让输入保持响应,但它不是节流,也不减少数据源更新。memo只有在 props 引用稳定且子树确实昂贵时才有收益。- Suspense 更适合异步边界和加载状态,不应替代流式协议本身。
5. 失败模式
- 旧流覆盖新消息:比较 conversationId、messageId、requestId 和 revision。
- delta 重复:使用 cursor 或服务端序号去重。
- 最后一片丢失:在 reader 完成时 flush buffer。
- 卸载后 setState:清理 reader、timer 和 abort signal。
- 列表越来越慢:虚拟化已完成的长消息,限制 DOM 节点与高亮频率。
可直接背诵的 30 秒回答
text
流式消息我不会只维护一个 text 和 loading,而是拆成传输、消息、视图和服务端状态。delta 先进入 buffer,再按帧或时间窗口批量 flush 到 reducer;消息按 conversationId 和 messageId 定位,旧 request 的事件要丢弃。React 侧通过拆分订阅、稳定 props、memo、必要时 transition 和虚拟化控制渲染成本。扩展知识
状态更新的目标
text
低延迟反馈 + 可丢弃过期事件 + 可重放最终状态 + 可控渲染成本useDeferredValue 的边界
它推迟的是使用该值的渲染工作,不会停止上游流,也不会替代 debounce、batch 或虚拟化。
面试官追问链
追问一:Context 能不能直接放 messages?
- 考察点:全局状态粒度。
- 回答方向:可以作为简单方案,但高频更新会让大量消费者重渲染;应拆 Context、使用 selector/store 或把流状态限制在局部。
追问二:什么时候使用 useTransition?
- 考察点:并发渲染理解。
- 回答方向:把不影响当前输入反馈的昂贵 UI 更新降级;它不适合处理网络请求取消,也不能保证后台任务一定立刻完成。
追问三:如何证明优化有效?
- 考察点:量化意识。
- 回答方向:比较 commit 次数、单次 render 时长、长任务、INP、内存、首 token 到首屏输出和低端设备数据。