Skip to content

第 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 的规范形态

text
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:

text
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:

  1. 命中 group name → Group;
  2. 命中 core node → Node;
  3. provider 整体不可用 → Unresolved(provider-unavailable);
  4. provider 中唯一同名候选 → Node;
  5. 没候选 → missing;
  6. 多候选 → ambiguous。

provider-unavailablemissing 必须区分。前者意味着数据获取失败,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 覆盖,但请求不会并发。成功后:

  1. 同步 tray;
  2. 增量记录 Profile selection;
  3. 若开启 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:

text
第一个消费者 mount → start worker
更多消费者 → 复用
消费者逐个卸载 → decrement
最后一个卸载 → terminate worker

这避免每个图表都维护一份采样历史,也避免页面离开后 Worker 长期运行。

十二、虚拟列表与 Canvas

项目使用 TanStack virtual、自定义 sticky virtual list 和 Canvas traffic graph。选择这些技术的共同原因是:

  • 节点/连接可能达到数千;
  • DOM 元素数量与布局测量会成为瓶颈;
  • SVG 路径频繁更新也可能比 Canvas 重;
  • sticky group header 需要在虚拟窗口外维持上下文。

性能优化不是单点:后端先规范化、store 结构共享、消息节流、Worker 压缩、最后才是虚拟渲染。

十三、实时系统的五层背压

text
Mihomo stream
  ↓ 共享 socket,避免重复流
消息层节流/批量

结构共享或时间序列压缩

外部 store / cache 精确通知

虚拟列表 / Canvas 只绘可见内容

只做最后一层虚拟列表,仍会浪费大量 parse、对象分配和 cache update;项目的价值在于背压贯穿数据链。

本章源码索引

  • src-tauri/src/core/proxy_view.rs::ProxyViewBuilder:规范化、解析与稳定排序
  • src/types/proxy-view.ts:前端版本化 schema
  • src/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:日志批量 flush
  • src/hooks/use-traffic-monitor.ts:Worker client 与引用计数
  • src/services/traffic-monitor-worker.tssrc/utils/traffic-sampler.ts:采样与压缩
  • src/components/base/sticky-virtual-list.tsx:大列表渲染

原创文档 CC BY-SA 4.0 · 站点代码 MIT