第 13 章:实时数据与 ProxyView——让高频状态可渲染
Mihomo 的原始代理对象、provider 节点和 group 引用并不适合直接给 UI 使用;连接和流量也不能每条消息都触发昂贵渲染。本章从后端 ProxyView 规范化到前端共享 WebSocket、结构共享、Worker 采样和虚拟列表,解释这条性能链路。
一、为什么需要后端 ProxyView
Mihomo /proxies 与 /providers/proxies 存在几个 UI 难题:
- group 和 node 混在同一 map;
- group member 只有名称引用;
- provider 中可能出现同名节点;
- provider 请求失败时,不知道是 missing 还是数据未到;
- HashMap 顺序不稳定;
- GLOBAL、DIRECT、REJECT 有特殊语义;
- runtime YAML 才保存用户期望的组顺序。
若每个 React 组件自行拼接,会产生不一致的去重、顺序和错误状态。因此后端构建版本化 ProxyViewV1。
二、ProxyView 的规范形态
schemaVersion: 1
orderSource: runtime | fallback
providerState: ready | unavailable
global: optional group
direct: optional record id
groups: ordered group list
records: recordId → node
standalone: record id list
providers: provider views节点和组分离。组成员不再只是字符串,而是 tagged union:
Group { name }
Node { name, recordId }
Unresolved { name, reason }三、record id 解决同名节点
核心节点使用 c:{index},provider 节点使用 p:{providerIndex}:{memberIndex}。用户可见名称仍保留,但渲染 key、delay cache 和引用解析使用 record id。
为什么不能直接 provider/name?名称可能含 /,provider 内也可能重复;index 基于确定性排序构建,更容易保证序列化稳定。
四、MemberResolver 的判定顺序
对 group 的每个 member name:
- 命中 group name → Group;
- 命中 core node → Node;
- provider 整体不可用 → Unresolved(provider-unavailable);
- provider 中唯一同名候选 → Node;
- 没候选 → missing;
- 多候选 → ambiguous。
provider-unavailable 与 missing 必须区分。前者意味着数据获取失败,UI 应允许重试;后者才说明配置引用真的悬空。
五、顺序为什么在后端确定
build_ordered_groups():
- 先按 runtime
proxy-groups顺序挑选存在组; - 去掉重复;
- 再把未匹配组按 BTreeMap 稳定顺序追加;
- runtime 一个有效组都没有时标记 fallback。
同一份输入即使 HashMap 插入顺序相反,输出和 record id 都应一致。测试直接比较两次完整 view。
确定性对 UI diff、缓存和测试都很重要。
六、前端从 View 构建渲染模型
use-render-list.ts 将 ProxyView 转成扁平/分组渲染列表,处理:
- group 展开和链式导航;
- hidden/filter/sort;
- provider 展示;
- delay 状态;
- empty state 的原因模型;
- compact/normal 布局。
数据规范化放在后端,视图偏好放在前端。前者是跨界面共享的领域事实,后者是用户交互状态。
七、代理选择使用 latest-wins 队列
用户连续点击 A、B、C 时,最想要的是最终 C,而不是让三个请求无序并发。
useProxySelection 保存:
pendingRequestRef:始终只有最新等待项;isProcessingRef:当前是否执行;- loop:完成当前后取最新 pending。
因此中间 B 可能被 C 覆盖,但请求不会并发。成功后:
- 同步 tray;
- 增量记录 Profile selection;
- 若开启 auto-close,异步关闭经过旧节点的连接。
这是 UI 高频意图的合适语义:serialize + coalesce,而不是 queue every click。
八、连接流用 external store 和结构共享
连接 WebSocket 是模块级单例,只有有订阅者时存在。两组 listener:
- 完整连接表;
- 只有 active count 的摘要。
消息先按约 50ms 节流,再 parse 一次。若只有摘要订阅者,就不构建完整连接对象。
8.1 结构共享
mergeConnectionSnapshot() 按 id 匹配旧连接:
- metadata 字段不变则复用旧 metadata 对象;
- chains 不变则复用旧数组;
- 整条连接完全不变则复用旧 connection;
- 计算本次
curUpload/curDownload; - 消失连接移入有上限的 closed list。
React 表格行可以通过引用相等跳过渲染。没有结构共享,即使 99% 连接没变,每个 WebSocket 快照也会制造全新对象。
8.2 useSyncExternalStore
连接 store 暴露 subscribe/getSnapshot,Hook 使用 React 官方外部 store API,保证并发渲染下快照一致,不必把每条消息搬进 Context。
九、日志流批量追加
日志消息会:
- 按当前 log level 过滤;
- 在 50ms buffer 中批量;
- 一批共享格式化时间,减少重复 dayjs;
- 最多保留 1000 条;
- 首次连接后合并后端已有日志快照;
- 切换 level 时重建订阅并重新加载。
单条 set cache 会造成每秒大量 render;批量 flush 以极小延迟换明显吞吐。
十、流量图为何使用 Worker
原始流量每个采样点包含 up/down/timestamp。长期保留全部点会增长内存和 Canvas 绘制成本。
TrafficDataSampler 维护:
- 短期 raw buffer;
- 长期 compressed buffer;
- compression queue;
- 按时间范围查询。
TrafficWorkerClient 优先创建 module Worker,把 append、clear、setRange、snapshot 消息发给 Worker。Worker 按 interval 合并 snapshot,再回主线程。
若 WebView 不支持 Worker或初始化失败,使用 InlineTrafficMonitor 同一协议降级。调用方不感知执行位置。
十一、引用计数管理 Worker
首页多个卡片可能同时需要流量。全局 ReferenceCounter:
第一个消费者 mount → start worker
更多消费者 → 复用
消费者逐个卸载 → decrement
最后一个卸载 → terminate worker这避免每个图表都维护一份采样历史,也避免页面离开后 Worker 长期运行。
十二、虚拟列表与 Canvas
项目使用 TanStack virtual、自定义 sticky virtual list 和 Canvas traffic graph。选择这些技术的共同原因是:
- 节点/连接可能达到数千;
- DOM 元素数量与布局测量会成为瓶颈;
- SVG 路径频繁更新也可能比 Canvas 重;
- sticky group header 需要在虚拟窗口外维持上下文。
性能优化不是单点:后端先规范化、store 结构共享、消息节流、Worker 压缩、最后才是虚拟渲染。
十三、实时系统的五层背压
Mihomo stream
↓ 共享 socket,避免重复流
消息层节流/批量
↓
结构共享或时间序列压缩
↓
外部 store / cache 精确通知
↓
虚拟列表 / Canvas 只绘可见内容只做最后一层虚拟列表,仍会浪费大量 parse、对象分配和 cache update;项目的价值在于背压贯穿数据链。
本章源码索引
src-tauri/src/core/proxy_view.rs::ProxyViewBuilder:规范化、解析与稳定排序src/types/proxy-view.ts:前端版本化 schemasrc/components/proxy/use-render-list.ts:渲染模型src/hooks/use-proxy-selection.ts:latest-wins 节点切换src/hooks/use-connection-data.ts:外部 store、节流与结构共享src/hooks/use-log-data.ts:日志批量 flushsrc/hooks/use-traffic-monitor.ts:Worker client 与引用计数src/services/traffic-monitor-worker.ts、src/utils/traffic-sampler.ts:采样与压缩src/components/base/sticky-virtual-list.tsx:大列表渲染