拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Web Shell 折叠思考流式渲染性能优化:结构快照、变更摘要与尾部投影

Web Shell 折叠思考流式渲染性能优化结构快照、变更摘要与尾部投影【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读在 Qwen Code 的 Web Shell 中模型流式输出时每一帧 token 追加都会触发 transcript store 通知而纯 assistant 文本追加与思考thought块尾部追加只需更新对应的消息行顶层App完全没有必要随之重新渲染。本文基于 docs/design/web-shell-collapsed-thinking-performance.md 的设计方案结合packages/web-shell的源码实现讲解 Web Shell 如何通过结构快照structural snapshot 变更摘要change summary 流式尾部投影streaming-tail projector 折叠子树懒挂载四层机制把流式渲染的 reconcile 成本限制在可见尾部并保证折叠的紧凑摘要行在隐藏期间不承载任何流式 props 的 reconcile 负担。读完本文你将掌握这套只更新可见尾部、不唤醒顶层组件的流式渲染优化设计以及它在当前仓库中的落地代码位置与验证方式。问题背景一次纯追加为何唤醒整个 Apppackages/web-shell/client使用useSyncExternalStore订阅 daemon 的 transcript store。网络上的每一个 chunk 到达后都会通知 store而通知会传播到所有订阅者。设计文档指出两个具体痛点纯 assistant 与 thought 尾部追加会唤醒顶层App实际上只有 transcript 中的那一行需要新文本App顶层却要重新跑一遍渲染与 reconcile紧凑活动摘要compact activity summaries在折叠时仍保持完整的工具与思考子树挂载隐藏行继续对不断流入的 thought props 做 reconcile白耗 CPU。在 App.tsx 中可以看到顶层组件通过useAnimationFrameTranscriptSnapshot({ structuralOnly: true })获取快照App.tsx#L3278-L3279而 ChatPane.tsx 则消费实时的节流快照。这正是文档中两个消费者各取所需设计的落地入口。双消费者模型App 与消息列表各取所需设计方案的核心是把 transcript 快照的消费拆成两条路径顶层 App 消费结构快照只关心结构性变化新增/删除块、工具调用、权限请求、会话切换等。当 store 的变更摘要证明本次更新只是对当前活跃 assistant 或 thought 块的纯尾部追加时这些 App 级通知被直接忽略消息列表MessageList单独消费实时的节流快照对 App 提供的最新结构消息应用现有的流式尾部投影器只更新可见尾部而不会启动第二条后台 agent 的 reconcile 循环。结构快照如何识别纯尾部追加实现位于 useAnimationFrameTranscriptBlocks.tsfunction isSameTranscriptStructure( previous: DaemonTranscriptBlockChangeSummary | undefined, next: DaemonTranscriptBlockChangeSummary | undefined, ): boolean { return ( previous ! undefined next ! undefined previous.source next.source previous.tailAppendBarrierRevision next.tailAppendBarrierRevision ); }关键字段是tailAppendBarrierRevisiondaemon 侧每发生一次结构性变更都会推进该屏障修订号而纯文本追加不推进它。订阅回调中useAnimationFrameTranscriptBlocks.ts#L87-L97const nextSummary store.getBlockChangeSummary?.(); const tailOnly structuralOnly isSameTranscriptStructure(previousSummary, nextSummary); previousSummary nextSummary; if (tailOnly) return; // 纯尾部追加直接忽略不调度渲染帧 if (frame ! null) return; pendingSinceTs performance.now(); frame window.requestAnimationFrame(dispatchWhenDue);structuralOnly模式下如果前后两次 summary 的source与tailAppendBarrierRevision完全一致说明这是一次纯尾部追加订阅回调直接return不会调度requestAnimationFrame顶层 App 保持休眠。只有下一次真正的结构性变化到来时才恢复调度。另外getSnapshot中同样利用了结构比较做快照缓存useAnimationFrameTranscriptBlocks.ts#L119-L130结构未变时返回缓存的快照对象避免下游useMemo因引用变化而重算。关于无变更摘要的 store 保持原行为文档明确Stores without a change summary retain the existing behavior。这一点在isSameTranscriptStructure中体现previous或next为undefined时函数返回false即无法证明是纯追加于是按结构性变化处理、正常调度渲染。也就是说变更摘要是可选的优化通道缺失时回退到全量渲染语义保证兼容性。50ms 渲染节流与输入静默窗口同一文件顶部定义了三个关键常量useAnimationFrameTranscriptBlocks.ts#L21-L23const TRANSCRIPT_RENDER_THROTTLE_MS 50; // 渲染节流窗口 const INPUT_QUIET_WINDOW_MS 100; // 输入静默窗口 const MAX_INPUT_DEFERRAL_MS 250; // 最大输入延迟容忍注释解释了动机流式期间每个网络 chunk 都通知 store而每次渲染都要跑一遍 O(transcript) 的归一化60fps 渲染会让每秒成本翻三倍而可见文本本身markdown 侧已按 80ms 节流不可能变化那么快50ms 窗口对流式文本依然流畅。dispatchWhenDue还通过navigator.scheduling.isInputPending()与beforeinput事件探测用户输入把渲染帧让位于击键等紧急更新useDeferredValue进一步保证流式帧不会阻塞 composer 输入。变更摘要驱动的流式尾部投影器文档指出消息列表单独消费实时节流快照并对 App 的最新结构消息应用现有的流式尾部投影器只更新可见尾部不启动第二条后台 agent 的 reconcile 循环。核心实现是 useMessages.ts 中的projectStreamingTailMessagesuseMessages.ts#L142-L242。投影器的工作方式projectStreamingTailMessages(previous, blocks, t, blockChangeSummary)接收上一次投影与最新块序列返回投影后的消息数组或undefined表示无法增量、需要全量重建。其判定逻辑分两层结构证明若前后都有变更摘要则要求source相同、revision严格递增、tailAppendBarrierRevision不变否则返回undefined。注意这里revision previousSummary.revision也会拒绝——修订号必须前进防止乱序或重复快照污染增量路径若任一侧缺少摘要则退化为逐块引用比较previous.blocks[i] ! blocks[i]即返回undefined。尾部块校验最后一个块必须是assistant或thought且前后 kind/id/streaming 状态一致meta、usage、branchRecordId、clientReceivedAt、promptId、sourceRecordIds全部不变文本只增不减after.text.length before.text.length即拒绝无摘要证明时还要求after.text.startsWith(before.text)。通过校验后增量路径只复制上一次的消息数组把新增文本拼到尾部消息上const messages previous.messages.slice(); const appendedText after.text.slice(before.text.length); messages[messages.length - 1] { ...previousTail, content: previousTail.content appendedText, isStreaming: true, ... };摘要证明与文本前缀两种证明的取舍有变更摘要且证明为尾部追加summaryProvesTailAppend true文本不再要求startsWith前缀关系因为 daemon 的屏障修订号已经证明是同一块的追加useMessages.ts#L158-L174无摘要只能靠新文本是旧文本前缀这一字符串证据来保守地证明追加useMessages.ts#L198。Insight 协议标记的全量投影保留前缀文档特别提到Insight protocol markers use a full projection while retaining the unchanged message prefix。代码中定义INSIGHT_CONTENT_MARKER insight_useMessages.ts#L48。注释说明Insight JSON 会把一个增长的文本块拆成多个投影消息因此一旦尾部文本出现该标记单纯增量不再成立必须全量投影但投影后仍会尽量复用前缀中 id 与 role 均未变化的旧消息对象useMessages.ts#L203-L221避免整条列表的对象身份失效引发下游重渲染。第二个 reconcile 循环如何避免useMessagesFromBlocks中当投影器返回增量结果且连接会话、解析快照均未变时直接复用消息数组跳过后台 agent 解析reconcileBackgroundAgentResolutions的重算useMessages.ts#L405-L415。同时它缓存 reconcile 所需的各类 keypendingBackgroundAgentKey、pendingPermissionKey、backgroundAgentNotificationKey等只要摘要的source与tailAppendBarrierRevision未变就复用从源头避免了流式追加期间反复发起后台 agent 的会话解析请求。MessageList 的增量更新判定消息列表侧还有一个独立的流式尾部纯内容更新判定函数isStreamingTailContentOnlyUpdateMessageList.tsx#L179-L215它同样要求除最后一条消息外所有消息对象引用相等、尾条 id/role 一致且处于isStreaming对 assistant 还要求branchRecordId、usage及文本空/非空状态不变。该判定被用于 viewport 自动滚动、hold 逻辑、虚拟列表布局等多种需要区分纯文本增长与结构性变化的场景如 MessageList.tsx#L2934、MessageList.tsx#L3375确保纯追加不触发页签重排、分页快照消费等重操作。紧凑折叠详情子树仅在展开时挂载文档的第三项设计是紧凑工具摘要仅在展开时挂载详情子树唯一例外是 MCP Apps——其 iframe 状态必须跨折叠存活折叠期间紧凑组不再保留隐藏详情行内的本地展开状态文档 Compatibility 一节明确 Collapsing a compact group no longer preserves local expanded state inside its hidden detail rows折叠按钮保持可点击live展开时从 props 重建当前工具与思考行而非恢复旧 DOM折叠/展开是即时、无动画的Collapse and expansion are immediate and unanimated避免过渡期间额外的布局与合成开销。在 ToolGroup.tsx 中可以看到compactSummary、expanded、detailsVisible等状态的设计ToolGroup.tsx#L90-L98以及工具行通过expanded || forceExpanded || !!hasSubToolApproval决定是否展示展开详情ToolGroup.tsx#L1276memo比较中detailsVisible变化会触发重渲染ToolGroup.tsx#L1054。这意味着折叠状态下的紧凑摘要行只需要渲染摘要按钮本身工具输出、thought 内容等详情子树根本不在 DOM 中也就不会对持续流入的流式 props 做 reconcile。兼容性约束设计方案保留了以下行为边界源码中均有对应变更类型处理方式工具tool、权限permission、终端、重置、历史、会话变更始终视为结构性变更走 App 全量通道立即替换基线快照transcript 回调如消息列表边界的 live 快照继续从 MessageList 边界接收实时节流快照语义不变无变更摘要的 store保留原有行为无法证明纯追加时按结构性处理折叠紧凑组的本地展开状态不再保留展开时从 props 重建结构性变化仍通过 App 流转并立即替换基线文档原文 Structural changes still flow through the app and replace the baseline immediately这与isSameTranscriptStructure中屏障修订号变化即触发requestAnimationFrame调度的行为一致。验证性能场景与单元测试设计文档给出的验证清单在仓库中均有对应落点结构快照忽略纯尾部追加并在下次结构变化时恢复单元测试 useAnimationFrameTranscriptBlocks.test.tsx 中的 keeps structural consumers asleep for pure tail appends 直接验证了这一行为——构造带tailAppendBarrierRevision的摘要确认结构消费者在纯追加时不重渲染throttles renders to one per throttle window 验证 50ms 节流窗口内的渲染次数caches structural snapshots when the store has no change summary 则覆盖无摘要时的缓存回退语义该文件 L132-L148、L157-L212、L261。折叠紧凑组无详情子树、展开恢复当前详情e2e 场景 web-shell.compact-thinking.spec.ts 中 compact view keeps the thinking block visible while streaming 验证流式期间思考块保持折叠、列表不包含私有思考内容、live 标签随阶段翻转compact view merges an agent group and a following tool into one summary row 验证 agent 组与工具合并为单行摘要的紧凑语义L15-L43。确定性折叠思考性能场景与定向单元测试useAnimationFrameTranscriptBlocks.test.tsx与 useMessages.test.ts、MessageList.dom.test.tsx、App.test.tsx 共同覆盖投影器与结构快照的组合路径可复现文档要求的确定性折叠思考场景。小结Web Shell 的流式渲染优化可以概括为四条相互独立的通道顶层结构快照忽略可证明的纯尾部追加屏障修订号不变即休眠、实时节流快照以 50ms 窗口与输入探测控制渲染频率、流式尾部投影器用变更摘要或文本前缀两种证明把消息列表更新收敛到可见尾部且不启动第二个 reconcile 循环、紧凑折叠懒挂载让隐藏详情子树在折叠期间零 reconcile。四者叠加把每个 token 都唤醒全应用的成本降低为只更新可见的那一行同时通过无摘要回退与结构性变更强制全量两条保险丝保证了语义安全。对这一设计做进一步性能验证或扩展时可直接以 useAnimationFrameTranscriptBlocks.ts、useMessages.ts 与两个测试文件为基准点展开。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门