# 渲染管线与 scrollback · 让长会话也跑得动

> 一个保留全部历史、还要流式刷新的滚动区，怎么不卡？grok 靠零字节空帧、估算-落定的懒布局、增量 virtual_y 补丁，以及一个专门改造过的 ratatui Terminal 分叉。这一章看它渲染快的几个核心手段。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/render-pipeline
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 渲染管线与 scrollback · 让长会话也跑得动

一个会话可能有几百条历史块，还在流式追加 token。全量重绘会卡死。grok 的渲染分两层：画帧、写终端的底层在 `xai-grok-pager-render`(`render/draw.rs`)，滚动区的布局与缓存在 `xai-grok-pager` 的 `scrollback/` 里。两层合起来，有一串让它跑得动的手段。

## 空帧不吐字节

核心在 `draw_frame`(它**绕过了 ratatui 的 `try_draw`**)：一帧的序列是「同步更新开始 → 取帧并渲染(顺带收集 OSC 8 链接)→ `flush_with_links` 做 cell diff，返回**有没有 cell 变化** → 判定空帧 → 写 PostFlush 转义(Kitty 图、通知等，仍在同步块内)→ 调光标 → 同步更新结束 → 整帧字节交给写线程」。关键：**没变化的空帧直接 early-return、一个字节都不发**——这才保住了**光标闪烁**(每帧都重发光标转义会打断闪烁计时：30fps 下，500ms 的闪烁周期永远走不完)。`CursorState` 还专门去重光标转义。

<Callout type="info">
  为此 grok 维护了一个 **ratatui `Terminal` 的分叉**(`xai-ratatui-inline`)。分叉的本意是 inline 视口：上游的 `Terminal` 不暴露操纵 inline 视口所需的内部状态。在此之上又加了几刀：让 `flush` 返回「这一帧有没有 cell 变了」、让 OSC-8 超链接参与 cell diff，还修掉了上游 `Buffer::diff` 在单元格数超过 65,535 时的 u16 溢出(420×160 的超大终端会把整个界面画进一个小角落)。**为了流畅和正确去改上游库**——这是产品对体验较真的程度。
</Callout>

## scrollback：全保留 + 懒虚拟滚动

滚动区**保留全部历史块**(不是环形缓冲)，滚动是**虚拟的**(只画窗口内的)。`LayoutCache` 存一份 `virtual_y`(各块累计行偏移的前缀和)，定位某个滚动位置在哪块用**二分**。

真正的性能核心是**懒测量**——「估算，再落定」：

- **批量加载/改宽**时，给每块一个**便宜的高度估算**(不精确)；
- 只对**视口窗口内**的块升级成**精确**高度(要真做 markdown 折行才知道)，窗口是可见块再往下多量 8 块；
- 一个刻意的设计：测量窗口**不含上边距**——这样向上滚时，测量不会把锚定的顶部推走。作为补偿，恢复会话、钉在底部重建时，会预先精确测量视口上方 3 屏，第一次上滚就不会跳。

## 流式追加不全重排

流式 token 进来，`push_chunk_to_agent` 这类方法把 chunk 写进块、清掉它的渲染缓存，再把那块标脏(`dirty_heights`)。下一帧走**快路径**：只重测脏块，只要没有折叠/展开/增删这类结构变化、只是高度变了，就**增量打补丁** `virtual_y`——对「尾部追加」是 **O(1)**，不重算整个布局。这就是「模型一直吐字、界面丝滑滚动」的底层。

还有几个配套：

- **`ScrollAnchor`**：改宽/重折行后，用「第几块、第几逻辑行、第几子行」重新锚定，而不是记绝对行号(重折后绝对行号就没意义了)。视口上方插入或删除块时，另有按稳定 `EntryId` 记录的 `StructuralScrollAnchor`，保证画面不跳。
- **渲染缓存**：每块的输出按 `(宽度, raw 模式, 主题, 是否选中, cwd)` 缓存(`RefCell` 内部可变)，内容一变就整条失效；滚出测量窗口 ± 128 块就**驱逐**重型缓存(每 5 秒扫一次，选中的块除外)，但保留高度不影响几何。
- **折行引擎**：基于 `textwrap`，带 **joiner** 元数据区分软/硬换行，好让复制时正确重接。

## 和《从 LLM 到 Coding Agent》对照

教学实现的「界面」不需要考虑这些——`print` 就完了。grok 的增量是把「保留全部历史 + 流式刷新 + 任意终端尺寸」做到丝滑：零字节空帧、估算-落定懒布局、增量 virtual_y、宽度稳定的锚点。为此不惜维护一个 ratatui 分叉。下一章看这些块里最复杂的内容：流式 markdown。

---

上一篇：[TUI 架构](https://daiw.org/manual/grok-build/tui-architecture) · 下一篇：[终端里实时渲染 markdown](https://daiw.org/manual/grok-build/markdown-render)
