OpenCode 的实验性 PR #40427 集中处理 Electron renderer 的响应性。作者在固定快照上报告:初始入口从 7.45MB 降到 1.82MB;Home 最坏 Long Task 从 135ms 降到三次冷运行均为 0;Composer 最大输入更新时间从 151.79ms 收敛到 10.61ms。
这些数字对应提交 1b0e4e4610 PR 当前仍为 Open,最新 head 又补了路由和缓存语义修复,没有公开同口径复测。它是一组可研究的实验,不是稳定版结果公告。
优化目标是连续可交互
Home 三次冷运行的完整完成时间分别为 2356ms、2014ms、1923ms。列表全部出现仍需约两秒。作者接受这项取舍,因为页面会跨 lazy chunk、Worker 和 animation frame 分阶段完成,过程中没有 100ms 级 renderer 大块阻塞。
整套设计可以拆成四种预算:启动代码与数据工作集、主线程纯计算、单帧 DOM 提交、无明确消费者的后台任务。
一、启动工作集按功能访问边界拆分
packages/app/src/app.tsx 使用 Solid lazy() 拆出 Session、Home、legacy layout 与 File renderer;desktop i18n 只让英语字典常驻,其余 locale 动态加载;titlebar 和 overlay 也有独立 Suspense。
const Session = lazy(() => import("@/pages/session-lazy"))
const Home = lazy(() => import("@/pages/home"))
const File = lazy(() =>
import("@opencode-ai/session-ui/file")
.then((m) => ({ default: m.File })),
)
入口体积因此沿 7.45 → 5.80 → 3.95 → 2.64 → 1.82MB 下降。首次访问懒模块仍需支付加载与求值成本,所以合理边界应对应真实功能路径,明确导航时再定向 warm。
二、Worker 既要转移输入,也要限制输出
兼容 SDK 的 Session 消息、兼容 VCS diff,以及 Home V2 session list,以 ArrayBuffer 获取响应,再将 buffer 作为 transferable 交给懒 Worker。
getWorker().postMessage({ id, type, buffer }, [buffer])
Worker 内执行 TextDecoder → JSON.parse → clean/sort/project。Home 数据还会按目录过滤、保留近期记录并限制为 64 条。
普通对象返回 renderer 时仍需 structured clone。若把巨型对象原样送回,内存分配、Solid store 写入和 DOM reconciliation 仍会阻塞。有效的边界由三部分组成:transfer 原始输入,在线程内完成纯计算并裁剪,只返回有界结构。
当前新 V2 messageApi 的 Session 路径仍在主线程 normalize,因此不能把这项优化外推到所有消息链路。
三、DOM 按 frame 分批提交
Home 最多保留 64 条 session,第一帧只挂 4 行,此后每个 requestAnimationframe 再增加 4 行。
const HOMESESSIONLIMIT = 64
const HOMESESSIONRENDERBATCH = 4
requestAnimationFrame(() => {
setVisible((n) => Math.min(n + HOMESESSIONRENDERBATCH, count))
})
完成态 Markdown 每批主动执行约 8ms,剩余 block 放到下一帧。renderGeneration 相当于取消令牌:新内容产生后,旧批次的下一帧会退出,避免过期 DOM 工作持续占用 CPU。
8ms 只是批次检查点。单个巨型 block 仍可能超预算;120Hz 屏幕的整帧也只有约 8.3ms。实现提供了抢占和取消位置,没有提供硬实时保证。
四、后台推测服从显式需求
项目元数据与 Home index 保持 eager。provider catalog、global config/path、active-session status 等任务先避开首屏,再由 timer 与 idle callback 兜底。Composer 或模型设置一旦显式调用 loadProviders(),会立即打开数据 gate。
Session history page 从 200 条降到 50 条,ingestion 前先 scheduler.yield();邻居 Session 不再自动预取,只有明确导航时 warm,且 chunk、并发、pending 和缓存全部有界。
yield() 只改变执行顺序,不会降低总 CPU。把批量从 200 缩到 50,才让 continuation 本身变小。
五、输入和 diff 优先收敛最坏尾部
Composer 的 p50 从 6.50ms 到 6.17ms,几乎持平;最大更新时间从 151.79ms 降到 10.61ms。测量修订把 Prompt 结构的解析结果集中计算并复用,重点解决复杂 contenteditable 状态下的异常峰值。
当前 head 已重组输入代码:parseFromDOM() 只构建一次 Prompt 并复用,getCursorPosition() 仍会 clone Range 并递归计算长度。当前实现不能表述为全程一次 DOM 遍历。
VCS diff 列表也不再提前携带所有完整 patch。列表只取摘要,选中文件后才加载完整或有界 diff。兼容 diff Worker 的结果若撞上连续输入,会等待最近一次 beforeinput 后 100ms 再 resolve,避免响应式更新挤进键击窗口。
性能数据如何获得
Profiler 运行 production Electron,使用固定 24 小时真实语料。17.31GiB 只读源数据库被裁为 25.2MiB partial snapshot,并记录 SHA-256;每轮复制独立工作数据库,再冷重启 Electron。
Long Task、LoAF、CPU profile 和 frame gap 被同时采集。三次 Home 测试还插入 80ms 人工校准任务,三次均被观察器捕获,排除了“0 Long Task 是监控失效”的简单解释。
剩余问题也写在报告里:一次 Home run 仍有 152ms LoAF blocking;大 Session 仍有 67–121ms Long Task;真实私有快照无法由第三方完整还原;当前 head 没有同口径复测。
总结
OpenCode 这组改动的共同语言是“有界、可中断、按需求发生”:启动只带当前路径需要的代码;跨线程处理时在线程内先裁剪;DOM 按帧批量提交并允许取消;后台数据服从显式用户意图。
迁移这些做法时,要继续追问成本被移到了哪里、Worker 返回对象是否有界、主线程恢复后是否仍有大批提交、性能数字属于哪个 revision。做到这一步,lazy、Worker、rAF 和 idle 才能组成性能架构。
热门跟贴