跳转到内容
Yuuki学习手记
GitHub

五条路线,十五次性能拆招

给同一份界面换一种更新方式,再在输出中打字、滚动、选字、按停止。你会追到长历史重复解析、整棵 UI 树跨桥、后台旧结果迟到这些具体现场。先选一个最想拆的场景,记下自己的预测,再切换路径看结果。

日期:2026-09-26。目标:亲手观察一种实现在哪里失去响应,优化具体消除了什么工作,以及为了保持这种性能必须维护什么。五个应用新增「场景实验」入口,保留 Agent、设置、会话、语音和回放。场景不依赖 DeepSeek、Whisper 模型、工作区文件或外部服务。

这组代码让你接触真实 SwiftUI/AppKit、GPUI、React 原生桥、WKWebView 和 Electron 机制。每条路线有三个场景,每个场景有两条可切换的实际执行路径。 “terrible” 指一种可解释的糟糕实现方式;它不是该框架必然有的缺陷,也不是通过额外忙等制造的性能结论。

本轮只做实现、源码阅读与构建,没有执行这些负载、打开应用、测量性能或使用 computer-use。五路线 build-all.sh 已成功,Electron 最后修改取消生命周期后也成功重新打包,最新产物见 examples。下面的“观察”是你运行后可以检查的现象;所有优化收益都还需要你的机器上的证据。指标初始为空或零,不预填漂亮分数。

  1. 从 examples 入口 选择应用,打开「场景实验」。不配置 API Key 也能操作这些场景。
  2. 选择一个场景和朴素路径,主动点击运行;有负载预设的路线先选较小值,其余使用页面标明的固定上限。在响应探针持续输入中文、选中文字,同时滚动输出或尝试停止。
  3. 看清实际完成条数、最终文本或指纹,记下相关计数。只看更流畅而遗漏事件、终态或草稿,不算正确优化。
  4. 用相同负载切换优化路径。再做相同操作,观察工作量、延迟与焦点行为。需要更明显的负载时,逐级增加预设。
  5. 阅读场景旁的维护代价,再打开对应源码。修改一个相关能力,观察优化假设是否仍成立。

开始时一次只运行一个应用。不同路线的实验数据规模、文本、布局和事件节奏并未统一,不能把十五个场景中的毫秒直接排成框架排行榜。尤其 GPUI 默认是 dev/debug 构建;若未来跨路线测量,先统一优化构建和负载。

路线 / 场景Harness 中的真实情境朴素路径暴露的问题优化路径的机制用户可以观察什么
S1 Swift · 长会话流式排版模型继续输出,用户回看旧消息每个增量重新解析全部历史 Markdown缓存完成历史、显式 revision、独立活动消息、合并发布输入与滚动、累计历史解析行数、活动消息发布数
S2 Swift · 工作区日志检索检索工具结果,快速修正查询MainActor 上归一化、筛选与排序缓存归一化索引、后台计算、取消和查询代际输入是否跟手、查询准备成本、旧结果是否覆盖新查询
S3 Swift · 工具控制台日志不断追加,用户在旧行选字每次替换完整 NSTextView 文本NSTextStorage 追加、选区和滚动位置维护被重新提交的文字量、选区、阅读位置
G1 GPUI · 长事件列表长任务累积数千条工具事件创建所有行的元素uniform_list 仅构建可见范围实际行构建计数、滚动、输入;不是只看 GPU 名字
G2 GPUI · 流式消息token 到来时历史本来没有变化全历史解析/格式化和大范围更新缓存历史、独立活动 Entity解析次数、处理时间、更新范围
G3 GPUI · 工具产物索引UI 收到大型结果,需要检索摘要UI 线程承担索引计算background_executor、协作停止、generation 校验输入、停止、结果指纹、过期结果处理
N1 React → AppKit · UI 快照每个 token 只改变当前消息每次跨 Node/Swift 发送完整 UI 树稳定节点 ID、属性 patch、原生局部应用实际 JSON 字节、消息与解码/应用成本
N2 React → AppKit · 多工具进度多个工具集中产生短事件逐事件发布 React/桥接状态合并显示批次,保留全部状态归约与终态收到事件数和发布次数的差别、完整最终进度
N3 React → AppKit · 动态审批表单用户填写备注时工具仍更新反复重建真实原生控件保留稳定 ID 与原生控件身份焦点、选区、输入法组合、草稿与控件重建计数
W1 WKWebView · 长日志 DOM浏览长任务历史全历史 DOM 和协调固定高度窗口,只挂载视口附近挂载数量、滚动与输入
W2 WKWebView · 流式会话解析新消息增长,旧块已完成每次重新解析整段会话缓存完成块、只处理活动文本实际解析工作量、正文一致性、发布成本
W3 WKWebView · 日志搜索大量工具文本准备索引Web 主线程解析和检索本地 Web Worker、取消与过期结果屏蔽输入响应、准备成本、搜索结果、终止行为
E1 Electron · 多工具 IPCmain 接到大量工具输出每条事件一条跨进程消息200 条批量;两边均有相同确认窗口数据消息数、JSON 字节、收到条数与指纹
E2 Electron · 主进程产物索引main 管工具,又做重计算控制 IPC 与取消排队同一计算函数放入 Node Worker控制请求 RTT、取消能否及时生效、启动与计算成本
E3 Electron · 长工具卡片长历史里有富文本式卡片完整 React 卡片与 DOM固定行高窗口 + memo实际挂载卡片/节点、首次提交、滚动与查找限制

这些场景有意围绕同几种 Harness 压力,但切入的边界不同。WebView 的 Worker 移走的是 Web 主线程计算;Electron 的 Worker 移走的是 main 计算。React 原生桥中的身份是 AppKit 对象身份;GPUI 的更新边界是 Entity 与元素构建范围。把名称相近的优化放回实际调用链,才能理解它们。

Swift:系统控件很好用,状态与线程仍由你负责

Section titled “Swift:系统控件很好用,状态与线程仍由你负责”

阅读 Swift 场景说明 与 ScenarioLabs.swift。场景按会话、搜索、控制台分离,具体入口和相关文件在路线 README 中。

S1 把“模型流式输出”具体化为固定 180 个文本块。两边都要交付相同最终正文;优化缓存已完成历史,避免下一块到来就重新解析几千条旧内容。它还每 6 块合并一次活动文本发布:名义输入间隔 30 ms,因此中间结果会更粗,不能把减少发布次数等同于所有体验都更好。停止时必须发布尾批。

S2 的重点是查询生命周期。将计算挪到后台之后,新的查询可能先完成,旧查询随后返回。你必须维护任务取消和代际校验;单纯给函数加上异步标记,不能替代这两项责任。缓存也需要明确何时失效。本例只处理固定内存数据,不承担真实文件系统监听和增量索引。

S3 是原生路线值得触摸的地方:文本系统允许追加文本而保留同一个视图。两条分支采用相同的 NSTextView 与选区/滚动恢复策略,没有故意让朴素分支丢失选区;比较的是实际文字写入量、处理时间,以及负载中的操作响应。自动跟随末尾、用户主动上滚、选择文本、内容上限、重置等行为都要有清楚规则。优化的收益来自更小的修改范围,不是换掉了 Swift 语言。

**维护练习:**让一条已经完成的历史消息可编辑。S1 的缓存失效、S2 的索引更新、S3 的文本区间更新会立即成为具体问题。记录你究竟需要改哪些状态所有者,而不是只记增加了几行 Swift。

Rust / GPUI:能控制绘制,不代表计算会自动避开 UI

Section titled “Rust / GPUI:能控制绘制,不代表计算会自动避开 UI”

阅读 GPUI 场景说明 与 scenarios.rs。本项目采用发布版 GPUI API;既不是把 Zed 的全部编辑器框架移过来,也没有用 WebView 代替 GPUI。

G1 将可见范围构建与全量构建放在同一份事件数据上比较。uniform_list 把行构建数量与视口联系起来,但固定行高、元素身份与数据访问方式仍影响体验。实际构建计数说明执行了多少行构建函数,不能反推出 GPU 帧率。

G2 让活动文本成为更小的更新单元。你可以沿 Entity、notify()、缓存和流任务看更新扩散到哪里。优化保留相同最终文本,减少历史重复工作和通知范围;这里没有实现额外的片段合批。两个模式每次消费后有相同的 16 ms 基础间隔,处理较慢会自然推迟后续消费,因此它不是固定墙钟速率的网络推送。

G3 的两个模式执行同一索引函数。优化通过后台 executor 保护 UI 线程,还需要协作取消、一个作业上限以及过期结果校验。Rust 的所有权帮助表达对象与任务关系;它不会自动决定你应该把工作安排在哪个线程,也不会替你设计恢复、背压和输入体验。

**维护练习:**把 G1 固定行改成可展开多行工具输出,或让 G3 在新查询到来时立即替换旧作业。你会直接接触 GPUI 布局假设、Rust 数据所有权与后台结果回到 UI 的路径。这些正是值得学习的技术内容。

React → 原生:最重要的边界在 React 树和原生对象之间

Section titled “React → 原生:最重要的边界在 React 树和原生对象之间”

阅读 React 原生桥场景说明。主要路径是 React 场景状态、custom renderer 编码、JSONL、Swift 解码、AppKit 属性与控件更新。

N1 用 1,200 条静态历史和 180 个流式块,使“本次只有一点变化,却发送整棵树”的成本可见。优化发真实 patch;Swift 也要真正只更新受影响控件。只缩小一侧的状态对象,另一侧仍全量重建,并没有完成这个优化。

N2 用 24 个工具、每个 20 次进度,共 480 个逻辑事件,分开接收与处理事件和发布用于显示的状态。显示可以合并,逻辑归约和最后终态必须完整。取消也需要先清理定时工作,再把最后状态发布出来。这个场景适合观察“每收到一个事件就要求 UI 提交一次”的代价。

N3 更直接:在审批备注里打字、选字或输入中文,同时让工具进度刷新。稳定 ID 的价值不止减少分配,它还为焦点、选区和正在输入的内容保留对象身份。朴素路径确实重建 AppKit 控件,优化确实复用;是否发生可感知的输入法问题,还需要实际操作。

**维护练习:**给审批表单增加一个可选择目录的字段。你需要追踪组件 props、wire 类型、patch、原生控件、回传事件和销毁。这里的成本是跨边界契约的协调,不宜用“React 写界面很快”或“原生控件很快”一句话概括。该桥是教学实现,并不是 Raycast 内部 renderer 的复刻。

WKWebView:省下随包浏览器,不会省下 DOM 与 JavaScript 的工作

Section titled “WKWebView:省下随包浏览器,不会省下 DOM 与 JavaScript 的工作”

阅读 WKWebView 场景说明。场景和原 Agent 视图使用真实 ReactDOM;W3 从固定打包源码创建 Blob Worker,CSP 仅为此允许 worker-src blob:,避免依赖 WKWebView 的本地 file URL Worker 加载行为。

W1 与 W2 是两种不同的成本:少挂 DOM 与少做解析不能互相替代。只把长列表虚拟化,仍可能每个 token 解析所有历史;只缓存解析,仍可能一次挂载所有复杂块。观察各自的计数,避免把一项优化的效果套到所有负载。

W3 的优化也不是免费的。Worker 启动、输入构造或复制、索引准备与回传都在真实路径中;第一次查询和索引就绪后的查询应区分。取消时要终止或停止旧工作,并丢弃迟到结果。Web 主线程顺畅之后,Swift 宿主桥仍可能有独立的延迟来源。

**维护练习:**把 W1 卡片改为可展开,把 W2 的历史块改为可编辑,再观察窗口测量和解析缓存都要维护什么。若用户需要跨长历史复制或系统查找,虚拟化减少挂载的同时,也会改变这些行为的实现成本。

Electron:控制面、跨进程流量和 DOM 是三类独立压力

Section titled “Electron:控制面、跨进程流量和 DOM 是三类独立压力”

阅读 Electron 场景说明;UI 在 scenes.jsx,main 的调度在 scene-controller.cjs,同一 CPU 工作函数在 scene-work.cjs,Worker 入口在 scene-worker.cjs。

E1 有 2k / 10k / 30k 事件。两种路径都使用 200 条确认窗口,防止朴素模式把无限队列压进 renderer;区别在于这个窗口内是一条一条发送,还是一次发送 200 条。所有事件都累计接收计数和指纹,预览只留最近 20 条。确认超过 30 秒会结束并报告错误。数据消息数与 JSON 字节是实际调用路径的计量;它们不包含操作系统封装开销,也不是网络带宽。

E2 有 20k / 80k / 200k 条产物记录。两个模式共用生成、解析、归一化、去重、排序和指纹函数。优化 Worker 内生成数据,只把摘要送回 main;因此它展示的是合理的计算归属,也明确避开了巨大结果的跨线程复制,不能据此声称这种复制成本消失。总耗时含 Worker 启动与退出,计算耗时单列。收到结果和线程退出是两件事,工作槽要保留到退出;取消也采用这个规则,避免重跑时旧工作仍在退出中。

E2 最需要观察的是工作台能否被控制。界面提供每 150 ms 的控制 IPC 探针,最多一个未完成请求。main 被同步计算占用时,即使 renderer 还有部分响应,取消也可能直到任务完成后才被处理;UI 会如实说明没有成功中断。Node Worker 路径可独立终止。Electron 官方也建议将长时间 CPU 工作移出 main;Worker 是运行边界,不是零成本的语法糖。Electron 性能说明、Node Worker 文档

E3 有 1k / 3k / 6k 张卡片,112 px 固定高度。优化只挂载视口与缓冲区,稳定卡片使用 memo;实际挂载卡片和 DOM 后代节点通过当前容器计数。首次提交计时包含数据生成和 React DOM 提交,止于 layout effect 阶段,不含浏览器绘制。朴素同步提交无法中途取消,结束后可以清空场景。

**维护练习:**给 E1 增加新的审批终态,给 E2 加大结果回传,再把 E3 改成可变高度。你会分别触及协议与背压、Worker 传输、视口测量。不要为保持漂亮数字忽略这些功能需求。

页面上的数字能支持的判断不能支持的判断
接收数、完成数、最终文本长度/指纹两个完成运行是否交付了可对照的输出;是否疑似丢事件指纹不是严格证明,也不是安全哈希;取消的部分运行不能与完成运行等同
解析行数、格式化次数、提交文字量优化是否减少重复工作不能直接推算用户可见延迟
元素/控件/DOM 数量本次实现实际构建、创建或挂载多少 UI 对象;具体口径看页面不是整套应用的内存、GPU 工作或帧率
状态发布次数应用请求 UI 更新的频率不等于 React commit、SwiftUI body 或实际呈现次数
JSON 字节、bridge 消息数协议载荷与跨边界调用规模不等于 OS 物理传输字节或进程 RSS
编码、解析、计算与提交耗时指定代码阶段的耗时;是否将昂贵工作移出关键线程不等于从按键到屏幕出现字符的延迟
IPC RTT控制请求被发送、排队、处理并返回所花时间不是纯粹的底层传输延迟

计数器本身也有开销。场景内的统计、JSON 大小计量、结果指纹和 UI 文案都是这份实验的一部分;不能把绝对耗时直接当成生产产品上限。页面的有限预设是教学负载,不构成吞吐能力声明。

下面是本次源码已经能指出的责任变化,不是推测的工时。精确入口见各路线 README。

优化类别新增的状态/契约最容易被新功能打破的假设下一项小改动
历史缓存与活动消息分离cache key、revision、完成/活动边界历史永不编辑、主题或字体不变编辑一条旧消息,正确失效该条缓存
固定行高窗口行高、可见范围、稳定 ID、滚动锚点每行等高,未挂载内容无需系统查找展开一张长工具卡片,保留阅读位置
属性 patch 与原生对象复用节点 ID、属性 schema、patch 应用与回传事件节点类型和 ID 一致,删除能回收增加一种可编辑审批控件
批量发布与 IPC 窗口待发布状态、批次边界、确认、终态只要最终值即可;所有事件都可合并加入不能被覆盖的审批/错误事件
后台计算任务所有者、取消、generation、结果上限返回顺序等于请求顺序、工作可被及时取消连续发两次不同查询,保留新查询结果
文本增量追加已应用长度/版本、选区、阅读锚点内容只追加、不截断不重写限制日志容量,移除最老内容并保留合理选区

一次维护练习可记录下面几个事实:理解入口花多久;实际修改哪些模块、语言和协议;编码和调试分别花多久;编译等待多久;哪个交互假设最初遗漏;最后是否仍保持正确的终态与取消。初次学习和第二次类似改动分开记录。不要从 LOC 反推出开发时长,也不要把多个 Agent 并行的会话时长当作单人工作成本。

这轮落地的是各应用内部的场景与局部计量。已有统一评估工具文档提出的 CLI、Instruments/trace 采集和跨语言静态报告,仍然是后续工作;本次没有把这些能力虚构成一个已经完成的全能分析器。

这些场景现在提供了明确的下一层输入:场景 ID、输入规模、实现模式、结束条件、局部工作计数和维护责任。以后要统一报告,应先增加统一的结果导出和构建元数据,再接各技术栈的 trace。首次呈现、真正卡顿、内存和空闲能耗需要对应采集证据,不能用现有计数代替。

当前最值得形成的判断是:哪一条路线让你更容易找到多余工作、收紧更新范围,并保持修改后的功能正确。 这是你可以通过代码与操作积累的认识,无需先选定 Swift 或 Rust,也无需把语言喜好包装成测量结论。

控制面的取消、断连与恢复场景,见 Harness:场景回归测试控制面。这些测试验证 Run/ToolCall 状态机,不替代上面的 UI 负载实验。

返回 文档入口 · examples · 五路线收益与代价。