9.9 KiB
9.9 KiB
GoodBuddy 性能原则
文档信息
- 适用范围: Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。
- 用途: 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。
- 关联文档:
docs/roadmap/product-performance-experience-improvement-plan.md(测量记录与实施进度)。 - 参照: VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。
1. 性能预算
“流畅”用下面的数字判断,不靠感觉。“标准机”指不降速运行 npm run perf:app,“低端机”指加上 GB_PERF_CPU_THROTTLE=4。
| 指标 | 标准机 | 低端机 |
|---|---|---|
| 按键到下一帧,p95 | ≤ 16 ms | ≤ 50 ms |
| 点击到界面响应(切换会话、打开面板),p95 | ≤ 100 ms | ≤ 300 ms |
| 稳定交互期间(打字、滚动、流式输出)超过 50 ms 的长任务 | 0 | 尽量为 0,总时长不超过场景时长的 5% |
| 流式输出、滚动时的帧间隔,p95 | ≤ 17 ms | ≤ 33 ms |
| Main 事件循环单次阻塞 | ≤ 16 ms | — |
| Main 事件循环延迟,p99 | ≤ 30 ms | — |
| DOM 节点数 | 不随会话数、消息数增长 | 同左 |
标准机的阈值由 npm run perf:check 检查。低端机的数值目前只做记录,等结果稳定后再加进阈值检查。
2. 原则
P1 Main 只做调度
Main 负责窗口、生命周期、权限和消息转发。数据库查询、文档解析、哈希、向量计算、批量文件读写,都放到 worker 线程或 utilityProcess 里执行。
- 新代码不能在 Main 里使用
DatabaseSync和同步文件 API。lint 已有白名单,名单只能减少。 - 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。
- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。
P2 界面状态局部订阅
状态按领域放进外部 store,组件通过 selector 只订阅自己用到的那一部分。
- 不再往
App.tsx里加useState、useEffect或领域逻辑。App.tsx有行数上限,只能往下调。 - IPC 订阅放在 store 模块里,组件不直接调用
window.goodbuddy.*.on*。 - 一个按键、一个流式片段,只能让显示它的那一小块界面重新渲染。
- 传给 memo 组件的回调和 context 值,引用必须稳定。
模块约定(便于维护,所有拆分都按这个结构做):
| 层 | 文件命名 | 内容 | 不允许 |
|---|---|---|---|
| Store | <domain>-store.ts |
create<Domain>Store() 返回 getState / subscribe / 领域动作;纯 TS,不依赖 React;可单测 |
访问 DOM、调用 React hook |
| 选择器 / Hook | <domain>-selectors.ts 或 use-<domain>.ts |
基于 useSyncExternalStore 的 selector,返回值引用稳定 |
在 selector 里新建对象却不做相等比较 |
| 同步层 | <domain>-sync.ts |
与 Main 的 IPC:首次加载、订阅推送、增量保存队列、失败重试、退出前刷盘 | 放在组件里;整份数据回写 |
| 视图 | <Domain>*.tsx |
memo 组件,只从 selector 和稳定的动作读数据 |
接收 App 的大对象或内联回调 |
App.tsx只负责布局外壳、路由和把 store 实例通过 context 提供给子树。新增领域状态一律走上表四层。- 每个 store 的单元测试和 store 文件放在一起(
<domain>-store.test.ts),覆盖顺序、失败重试和边界情况。 - Main 侧对应:
<domain>的数据库访问集中在一个 repository 类里,IPC handler 只做校验和转发。
P3 高频数据走专门通道,按帧合并
流式文本、终端输出、进度、滚动这类高频数据,不进全局状态。
- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。
- 高频路径上不能读取布局(
getBoundingClientRect、scrollHeight等),布局读取要集中到每帧一次。 - 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。
P4 数据只有一份来源,只传变化部分
- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。
- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。
- 写入合并成批次和事务,不按每个事件单独开一个事务。
P5 工作量和可见内容成正比
- 长列表只渲染可见部分,包括消息、会话、笔记、文件树和知识库列表。
- 解析和渲染的结果要缓存,比如 Markdown 语法树。内容没变,就不重新计算。
- 增量处理:只解析变化的部分。没有把握时,退回到慢但正确的完整处理,并记录在测试里。
P6 启动只做首屏需要的工作
- 首屏之前只做显示应用壳和恢复上次界面所需的工作。
- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。
- 后台模块在第一次使用时才加载。
P7 先测量,再修改
- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。
- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。
- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。
P8 正确性优先于速度
- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。
- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。
- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。
P10 体验不倒退
性能优化只能让体验变好,不能用体验换速度。“行为不变”不仅指数据和结果,也包括用户能感知到的交互:
- 动画和过渡保持原样。 平滑滚动(如“到底部”按钮)、展开收起、悬停、淡入淡出都不能因为优化而消失。任何瞬间写入
scrollTop/scrollTo的代码都会打断正在进行的平滑滚动,必须在平滑滚动期间暂停。 - 只渲染可见部分(P5)时,用户状态不能丢。 行被卸载后,它的局部状态也会消失。以下行必须保持挂载,或者把状态存到行外:有焦点的行、用户展开过
<details>或输入过内容的行、选中文本的两端、待回答的问题和待审批项、打开菜单或正在重命名的行。 - 只渲染可见部分时,基本操作要和完整列表一致: 跨行选择和复制、滚动位置不因上方插入或删除而跳动(关闭原生
overflow-anchor时要自己做锚定)、快速拖动滚动条时没有空白帧、渲染结果缓存让重新挂载的行不出现“加载中”。 - 合并和批处理不能增加交互延迟。 第一条数据立即处理(leading edge),只合并之后同一帧内的数据。打字回显、终端回显不能多等一帧。
- 低频同步不能让用户看到旧数据。 复制、导出、保存这类读取全局状态的操作,要先把待同步的数据刷进去。
- 体验要有测试。 涉及动画、滚动、焦点的改动,需要真实 Electron 测试(逐帧采样)或单元测试,并确认测试在旧代码上会失败。
P9 用工具守住规则
- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。
- 规则只能收紧,白名单和上限只能往下调。
- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。
3. 评审清单
提交性能相关改动,或者会影响热点路径的功能改动时,逐项确认:
- Main 里有没有新增同步数据库调用、同步文件 IO,或者耗时可能超过 16 ms 的计算?
- 有没有往
App.tsx里加状态?一次按键或一个流式片段,会让多大范围的界面重新渲染? - 高频路径上有没有读取布局,有没有每次事件都提交状态?
- 有没有整份数据回写,或者“收到通知后重新查全量”?
- 列表的 DOM 数量会不会随数据量增长?
- 有没有在启动阶段新增同步初始化?
- 是否附上了
perf:app、perf:main或perf:knowledge的前后对比? - 顺序、取消、持久化和恢复,有没有测试覆盖?
- 动画、平滑滚动、过渡还在吗?有没有新增会打断平滑滚动的瞬间滚动写入?
- 只渲染可见部分的列表,滚走再滚回来,展开状态、输入内容、选区、焦点还在吗?
- 合并或延迟处理后,第一次响应(打字回显、点击反馈)有没有变慢?复制、导出读到的是不是屏幕上的最新内容?
4. 当前差距
截至 2026-10-03,按原则对照的主要差距:
| 原则 | 差距 |
|---|---|
| P1 | 写入和大量同步数据库调用仍在 Main;没有独立数据进程。文档解析、分块和大文件哈希已移到 worker |
| P2 | App.tsx 约 7,900 行。输入区、隐藏路由、任务和结果事件、切换会话已不再整体重渲染 App;当前会话的流式吸收(每 250 ms)、队列和活动记录仍经过 App |
| P3 | 已基本满足:流式输出和终端输出都已按帧合并 |
| P4 | 活动记录已改为增量保存、按页读取(界面只保留已加载的页,计数和分组来自 Main 的汇总);部分 *Changed 通知之后还会重新查全量 |
| P5 | 已基本满足:消息、会话、笔记、文件树、知识库文档表格都只渲染可见部分 |
| P6 | 已有基线(npm run perf:startup):启动到输入框可用约 1.0 秒,Main 模块加载约 415 ms、本地工具环境初始化约 155 ms,还没有优化 |
| P9 | 性能阈值还没有接入 CI,也没有真实低端机的基线 |