# Zhihu Performance Regression Review

> Review and fix Zhihu++ functional regressions caused by performance optimizations. Use when a change trades correctness, selection, rendering, scrolling, navigation, or state completeness for laziness, caching, recycling, batching, deferred work, or reduced data structures; also use when deciding whether restoring older behavior would really regress performance. Requires cache-lifetime analysis, real-path staged benchmarks, functional regression coverage, and before/after evidence before implementation or PR delivery.

- Skill: `zly2006/zhihu-performance-regression-review` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add zly2006/zhihu-performance-regression-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zly2006/zhihu-performance-regression-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: zly2006 (https://skillmd.com/u/zly2006)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zly2006/zhihu-performance-regression-review

---


# Zhihu++ 性能回归复核

## 交付门禁

回归修复必须先在相对基线运行同一测试并确认失败，再验证修复后通过；不得用编译成功、静态推理或只覆盖新 helper 的测试替代红到绿证据。一次性逻辑应留在真实调用点。

把“功能正确”和“性能不回退”作为同一个验收目标。不要默认保留现有优化实现，也不要默认恢复旧实现；先用真实生命周期和测量结果选择改动最小、契约最完整的方案。

### LazyColumn 列表动画边界

`animateItem` 只有挂在 `LazyColumn` 的直接 item 根节点上，才能观察到 item 重排或移除后的位移动画。把它放在 item 内部卡片、`AnimatedVisibility` 或卡片自身上，只会影响子树布局，不能驱动兄弟 item 的列表动画。排查时先确认 `item(key)` 的根节点和稳定 key，再检查状态列表是否以不可变新列表触发重组。

## 1. 固定基线

1. 记录当前分支、回归来源 commit/PR 和修改前工作区状态。
2. 在改代码前运行用户感知路径的基准，保存设备、变体、输入、预热次数和原始样本。
3. 写出被破坏的产品契约，例如“全选覆盖完整文档”，不要把当前实现细节当成契约。
4. 增加一个修改前必定失败、修复后通过的最小功能测试；测试必须经过修改前失败验证。

## 2. 拆解缓存生命周期

逐层回答以下问题，并用源码、计数器或计时验证：

- 缓存保存什么：输入转换、AST、测量结果、布局结果、绘制结果，还是仅占位高度？
- 缓存归谁持有：进程、页面、父组合、子组合、列表项还是单次调用？
- 什么事件会失效：宽度、字体、主题、节点内容、滚出视口、导航或进程重建？
- 回收后重建是否命中缓存？Compose `remember` 只在对应组合仍存活时有效，不能把父层高度缓存误认为子内容布局缓存。
- 缓存命中是否仍执行测量、布局或绘制？对象复用不等于用户路径没有成本。

至少区分四类样本：

1. 冷启动首次执行。
2. 同一存活组合内重复执行。
3. 内容回收或组合销毁后重新创建。
4. 用户触发完整能力时的按需执行，例如全选、跳转或滚回已读区域。

如果强化回归测试覆盖的是用户真实会连续执行的动作，CI 失败应视为实现尚未闭环，不能通过删除动作或缩窄产品契约换取通过。先确认失败是否暴露了缓存回收、身份重建或状态跨动作丢失，再修实现；只有证据证明该动作不属于受支持行为时，才能调整测试。例子：长文全选后继续滚动检查范围再复制是正常使用路径，离屏节点在滚动中更换导致复制为空时，应稳定选择数据或节点身份，不能退回只测“全选后立即复制”。

完整能力存在多个等价入口时，先列出并验证它们是否共享同一个状态机，不能只为其中一个入口增加旁路状态。例子：全文范围既可能来自“全选”，也可能来自用户持续拖动选择手柄；如果只在点击“全选”后切换到完整文档状态，拖动路径仍会丢失离屏内容，而且同一份选区会出现两套互不兼容的语义。应让长按、拖动、全选、滚动和复制共享同一个选择源，并分别覆盖至少一个菜单入口和一个直接交互入口。

文本选择修复必须保存并实际查看进入选中状态后的截图，不能用“长按命令执行成功”、复制结果正确、测试 tag 存在或未选中页面截图代替视觉证据。截图至少要证明选区背景覆盖了目标字符，开始和结束手柄处于合理位置；涉及划线、高亮、链接等特殊 inline 节点时，还必须在该节点上真实长按并拖动一次，再截图检查选区是否能进入、跨出和保持可见。没有看过这些选中态画面，不得宣布选择问题已修复。

## 3. 分阶段测真实路径

先定位阶段成本，再解释端到端数字：

- 输入解码或 HTML 转换
- 文档结构构建
- 文本、公式和富内容测量
- 布局
- 绘制与首帧
- 回收、重建和交互动作

基准必须命中真实输入和真实 UI 路径；不要预构造结果、跳过布局绘制或只测无关内部函数。先预热，再在同一设备、构建和输入上采集多次原始样本。报告中同时保留中位数和样本分布，不用单次最好结果下结论。

功能测试和性能测试只要 Compose JVM 能覆盖，就优先在 JVM 上执行，减少 AVD 构建、启动和占用；JVM 性能数字不能直接当作手机性能结论，必须定期用同一输入、同一测量边界在真实手机或少量 AVD 样本上校准倍率，再用该倍率换算手机门槛。AVD 只用于建立或刷新倍率、验证平台特有行为和最终抽查，不能每轮优化都依赖 AVD。例子：桌面端完整布局与绘制为 30 ms，只有同场景 Android 样本证明倍率约为 3 倍时，才能把它对应到约 90 ms 的手机预算；换机器、Compose 版本或渲染路径后应重新校准。

## 4. 比较候选方案

至少审查以下三类方案，删除无法证明必要性的分支：

1. **恢复完整行为**：如果重复布局确实被有效缓存，优先恢复自然、语义完整的实现。
2. **按需完整化**：首屏保持惰性，在用户触发完整能力时再 materialize；必须测触发延迟，并确认公开 API 能稳定表达该状态机。
3. **数据与视觉解耦**：布局保持惰性，完整文档能力由 AST 或状态层提供；必须验证选择范围、复制内容、无障碍和语义树仍符合产品契约。

以下信号说明方案过于 hack，应继续寻找替代方案：

- 用不可见 UI 冒充数据层状态。
- 依赖内部 API、时序等待或上下文菜单实现细节。
- 只修“复制出来的字符串”，却没有恢复用户看到的选择范围或手柄行为。
- 为保留性能丢失结构语义、格式或无障碍信息。

如果相关代码已经用可复现数据注明“恢复全文布局”会造成秒级首帧或 OOM，就必须把这条证据当成候选方案的硬性否决条件，不能因为删除现有 hack 后代码更短而重新提出全量物化。此时应修复惰性结构与框架状态的边界，让真实渲染节点继续作为唯一内容源；代码简洁只在正确性和已验证性能边界内比较。例子：离屏富内容已证明不能常驻时，应让选择状态跨节点卸载保存并在节点回来时重接，而不是关闭延迟渲染换取默认选择。

不过，不要仅因实现形式不漂亮就否决它。如果公共框架没有表达完整契约的接口，窄范围、带回归测试且性能证据充分的桥接方案可以保留；必须在代码注释中说明框架边界和触发条件。

## 5. 决策与交付

选择同时满足以下条件的唯一方案：

- 功能回归测试通过，覆盖真实长内容和离屏部分。
- 修改前后的首帧、滚动、回收后重建和目标交互均有可比较数据。
- 没有依靠与瓶颈无关的微基准宣称“无回归”。
- 新增状态、抽象和请求都对应已验证的产品行为；删除薄包装和猜测性兼容。
- 按项目规定完成构建、格式化、设备验证和 PR 检查。

在 PR 中写清：根因、缓存生命周期、原始性能样本、功能测试的修改前失败证据、最终方案为何优于其余候选。不要只写百分比或“体感无变化”。

