# 什么时候才该上重量级方案：给阈值，不给意见

> “等测量要求时再引入”这句话，在你说出测量什么、阈值多少、在哪台设备上测之前，是没有用的。本文给出每个重量级元素的具体数字，以及那一类根本不该等阈值的决定。

- Canonical (HTML): https://kaguc.com/blog/adoption-thresholds-zh/
- Date: 2026-08-29


*English version: [When to adopt the heavy machinery: thresholds, not opinions](/blog/adoption-thresholds/)。这是[交易类 App 架构](/blog/trading-app-architecture-zh/)的判断标准篇。*

这个系列的每一篇结尾，都写了某种版本的“等测量要求时再引入”。就这么说出来，这条建议是没有价值的。它没有指明指标、没有阈值、没有设备，于是在实际工作中，这个决定最后由会上讲话最自信的那个人做出。

所以这里给出数字。有些是算术，放之四海皆准；有些来自感知研究；有些是行业惯例，我会标明它是惯例。请根据你的产品去校准，但校准的应该是**数字**，不是感觉。

在此之前，先做一处比任何阈值都重要的更正。

## 不是所有东西都该等阈值

这个系列前面暗示所有事情共用一条决策规则。这是错的，而且是这个错误里更危险的那一半：把“等需要了再加”用在错误的类别上，产出的是一次重写，或者一次损失。

把每个候选项分进两个盒子。

**A 盒：等待的代价有上界。** 你以后再加，成本和现在加差不多。性能相关的几乎全在这里：isolate、二进制编码、原生接收、自绘图表、功能 package。如果你跳过了而且判断错了，症状是**慢**，而慢是可见的、可测量的、下个迭代能修的。

**B 盒：等待的代价没有上界。** 事后补要么意味着改动每一处调用点，要么等你察觉时损害已经无声地发生了。这些在任何规模下都在第一天建好，不存在阈值讨论：

- **不可逆动作上的幂等键**，且在第一次发送之前就落盘。事后补意味着审计每一条写入路径，而在你补完之前，重复请求已经在生产环境里发生了。
- **每个请求都在服务端做授权。** 一个信任客户端的接口，从它上线那天起就是一个已经存在的漏洞。
- **任何“丢包即损坏状态”的流上的序列号。** 没有它，你连事后**检测**这个损坏都做不到。
- **重连与重新订阅。** 事后加会牵动每一个订阅点，而它的缺失看起来和应用正常工作一模一样。
- **金额用十进制，不用二进制浮点。** `0.1 + 0.2 != 0.3` 在账本里是一个舍入错误。把已持久化的结构从浮点迁走极其痛苦。
- **金融动作的只追加事件日志。** 你无法重建一段你从未记录过的历史。

B 盒的判据是：**如果缺了它，失败是无声的吗？它会损坏数据吗？补上它需要改动每一处调用点吗？** 任何一个“是”，就是第一天做。

下面全部内容只针对 A 盒。这个区分，才是“什么时候该上重的东西”真正的答案。

## 先定义目标

一个阈值要有意义，需要三样东西，而大多数团队一样都没写：

**带百分位的指标。** 平均值恰好会掩盖你真正在意的问题。二十帧里有一帧耗时 40 毫秒，在均值里看不见，人却看得很清楚。用 p95 和 p99。

**一台基准设备。** “在我手机上很流畅”不是数据。选一台具体的、大约三年前的中端机，把它定为准入门槛。旗舰机会让你通过一堆测试，而其中相当一部分用户会失败。

**一个真实的负载。** 生产环境的消息速率、生产环境的目录规模，以及至少 30 分钟的会话时长；泄漏和缓存增长在两分钟的测试里是看不见的。

写全了，一个可用的目标长这样：**在基准设备上，以生产速率喂数据、屏幕上 200 只可见标的、运行 30 分钟后，UI 帧耗时 p95 低于 16 毫秒、p99 低于 24 毫秒。**

现在这个阈值可以被违反了，而这正是它从一个意见变成一条决策规则的原因。

## 帧预算，这部分是算术

60 赫兹下，你每帧有 **16.67 毫秒**做完所有事；120 赫兹下是 **8.33 毫秒**。而且有两条线程都要塞进去：UI 线程（你的 Dart、构建、布局）和光栅线程（把结果画成像素）。

给 UI 线程的工作大约留**半帧**，也就是 60 赫兹下的 8 毫秒左右。其余留给布局、绘制、光栅化，以及任何真实系统都会有的尖峰所需的余量。如果你的 Dart 工作经常超过每帧 8 毫秒，那你离掉帧只差一次小小的波动，而掉帧恰好会聚集在应用最忙的时候。

这就是下面所有数字都以“毫秒每帧”表达的原因。它是唯一真实存在的预算。

## 逐项阈值

**把解码挪到 isolate**：当 DevTools 时间线显示，解析在 p95 下持续占用**每帧超过 2 到 3 毫秒**时。

低于这个数它只是噪声；高于它，你就把三分之一的 UI 预算花在了解析上。但要先走下面那个“便宜修法阶梯”。另外注意，isolate 之间发送的消息是**拷贝**的，所以一个话多的 isolate 可能比不用 isolate 还慢。要成批发送，用 `TransferableTypedData` 做零拷贝转移，或者用 `Isolate.run`——它把结果转移回来而不是拷贝。

**换二进制协议**：满足以下任一条件时。在**合并之后**，JSON 解析仍超过每帧约 2 毫秒；移动网络下持续载荷超过约 100 KB/秒；或消息速率超过大约每秒 1000 条。

在这之下，JSON 的成本真实存在，但不是你的瓶颈，而且它换来的可调试性是二进制格式会拿走的。

**上原生（FFI）**：当 Dart 在**用了 isolate 和合并之后**仍然满足不了预算时。现实中大约是每秒 **10000 条消息**以上，或者是大数组上的数值计算，比如在十万根以上 K 线上计算指标、且需要亚毫秒级的确定性。

一个诚实的校准：在每秒几千条的量级上，Dart 完全够用；而且[合并模式](/blog/flutter-highfreq-state-zh/)已经让你的渲染成本与数据速率无关。大多数在这个节点伸手要原生的产品，有的是合并问题，不是语言问题。

**这里还有一条反向阈值：** 如果团队里没有人能在两个平台上维护 C++，那么无论测量结果如何，答案都是不上。一个没人维护得了的优化是一份负债，而这也是唯一一处“团队能力”可以正当地压过数字的地方。

**上本地数据库**：当目录超过大约 **5000 到 10000 行，或约 1 MB**，或者只要搜索必须离线可用。

通常还有一个感知层面的论证会更早地定下结论：交互在大约 **100 毫秒**以内才会感觉是即时的（Nielsen 的经典阈值：0.1 秒即时，1 秒思路不断，10 秒注意力丧失）。移动网络的 p95 往返时延很少能塞进 100 毫秒。所以如果边输入边搜索必须感觉即时，那么本地化就不是优化，而是唯一能满足这条需求的设计。

**自绘图表**：当图表库表达不了你需要的东西时（联动的指标副图、画线工具、精确的十字光标语义），或者渲染元素超过大约 **2000 个**，或者序列以帧率更新。在这之下，用库。

**抽出功能 package**：当同一个代码库上的团队达到大约 **4 到 5 名开发者**，或增量构建超过 **60 秒**左右，或同一区域反复产生合并冲突（比如每月三次以上），或某一层需要被隔离测试时。

这些是惯例，不是物理定律。底层变量是协调成本，而它随人数上升，不随代码行数上升。Conway 的预测给了你一个合理性检查：模块数量大致应该等于团队数量。

**上共享跨平台核心**：当你有**两个或更多互相独立的客户端代码库**，并且有一批“分叉即缺陷”的领域规则时。如果一个团队用一份 Flutter 代码同时交付两个平台，共享其实已经发生了，再加一个共享核心是在解决你没有的问题。

**Add-to-app** 根本不是一个阈值，而是一种处境。当一个既有原生应用必须一边持续交付一边迁移时，你才采用它。如果没有可嵌入的宿主，这个问题不成立。

## 先走便宜的修法，按顺序

大多数阈值被突破的情况，根本不该走到架构改动那一步。沿这个阶梯往下走，在第一个能解决问题的台阶上停下：

1. **少做事。** 更小的载荷、更少的字段、查询加 `LIMIT`、分页、更少的可见项。通常收益最大，而且永远最便宜。
2. **少做几次。** 合并、防抖、缓存、记忆化。单是[按帧合并](/blog/flutter-highfreq-state-zh/)一项，就能把很多“我们需要原生”的处境变成不存在的问题。
3. **在更小的范围里做。** 收窄重建、拆分重绘图层、减少失效区域。
4. **换个地方做。** 挪到 isolate，或者挪到服务端。
5. **换个更快的语言做。** 原生、FFI。

第 5 级正是这个系列里大部分“重家伙”所在的位置。在没试过 1 到 4 就直奔第 5 级，是团队最终得到一套复杂架构、性能却没变的最常见方式。

## 即使过了阈值，什么时候仍然不该上

四个反向信号，每一个都应该让你停下：

**你运维不了它。** 没有演练过轮换、也没有熔断开关的证书固定，是在等某张证书过期时给自己制造一次故障。没有维护者的原生代码，是一个你修不了的缺陷。能不能把一样东西跑起来，本身就是它成本的一部分。

**触发的是尖峰，不是水位。** 开盘时的糟糕一天不算突破阈值。要求这个违反是持续的，看一周的真实使用，而不是一次性能剖析。

**更便宜的台阶还没试过。** 如果你没做过上面的 1 到 4，你还不知道自己面对的是一个问题，还是一份负载。

**它是一扇单向门。** 可逆的决定值得快速、廉价地讨论；难以回头的决定值得更慎重。一个下午就能撤销的二进制协议是双向门；一个已持久化的数据结构、一份公开的 API 契约、或者一个已被三个团队依赖的原生模块，则不是，它们值得走这个流程的慢速版本。

## 决策程序

1. 在你需要它之前，先写下目标：指标、百分位、基准设备、负载。
2. 在真实条件下测量。不是合成基准测试，不是旗舰手机。
3. 一旦违反，从阶梯第 1 级开始往下走。
4. 如果阶梯已走完而违反是持续的，就采用；但先过一遍那四个反向信号。
5. 记录这个决定：什么触发了它、你测到了什么、以及你需要看到什么才会撤销它。半年之后，这就是“一套架构”和“一堆沉积物”的区别。

第 5 步比它看起来重要。大部分糟糕架构并不是一个糟糕的决定，而是一个没人能重新检视的决定，因为当初的推理从未被写下来。

## 那么，只有 MVP 吗？

不是，而且它不该被读成上限。那个[参考 MVP](/blog/flutter-trading-mvp-zh/) 是与它的约束相匹配的正确**起点**，那篇里的升级表指明了各处接缝，而本文提供了触发每一次升级的数字。两者合起来描述的是一条路径，不是一个终点。

大致分四档，你可以据此定位自己：

| 档位 | 负载与团队 | 跑什么 |
|---|---|---|
| 1 | 每秒 10 次以下更新，1 到 2 人 | 一份代码库、REST 加 WebSocket、JSON、内存缓存、图表库 |
| 2 | 每秒约 100 次更新，2 到 5 人 | 加：按帧合并、带增量同步的本地目录库、自绘图表 |
| 3 | 每秒约 1000 次更新，若干团队 | 加：解码 isolate、二进制编码、功能 package；若有两份代码库则加共享领域核心 |
| 4 | 每秒 10000 次以上，多团队，受监管 | 加：原生接收、自研传输、模块注册表、完整审计与对账 |

这张表有两点值得注意。每一档都是前一档的超集，这正是这条路走得通的原因：没有东西被丢弃，只是把接缝填上。以及，本文开头那个 B 盒适用于**每一档**，包括第 1 档。一个只有一名开发者、但会下真实订单的应用，对幂等键和服务端授权的需要，和第 4 档系统完全一样多。

这就是完整的判断框架：**正确性没有阈值；性能有；而阈值是基准设备上的一个数字，不是一个意见。**


## 参考

*API 与渲染器相关断言按 Flutter 3.47.0 / Dart 3.13（2026 年 8 月）核对。*

- J. Nielsen，["Response Times: The 3 Important Limits"](https://www.nngroup.com/articles/response-times-3-important-limits/)，1993——0.1 秒、1 秒、10 秒。
- J. Dean, L. A. Barroso，["The Tail at Scale"](https://research.google/pubs/the-tail-at-scale/)，*CACM* 56(2)，2013——为什么描述用户体验的是分位数而不是平均值。
- J. Bezos，[Amazon 2015 年致股东信](https://s2.q4cdn.com/299287126/files/doc_financials/annual/2015-Letter-to-Shareholders.PDF)，2016——Type 1（单向门）与 Type 2（双向门）决策。
- M. E. Conway，["How Do Committees Invent?"](https://www.melconway.com/Home/Committees_Paper.html)，*Datamation*，1968——系统结构会复刻构建它的组织的沟通结构。
- D. Goldberg，["What Every Computer Scientist Should Know About Floating-Point Arithmetic"](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)，*ACM Computing Surveys* 23(1)，1991——为什么二进制浮点无法表示十进制的钱。
- Dart 文档，[Isolates](https://dart.dev/language/isolates)——isolate 之间的消息是拷贝的；`Isolate.run` 则转移其结果。另见 [`TransferableTypedData`](https://api.dart.dev/dart-isolate/TransferableTypedData-class.html)。
- Flutter 文档，[Use the Performance view](https://docs.flutter.dev/tools/devtools/performance)——UI 与 raster 时间线、帧预算与重建计数器。

---

*返回[总览](/blog/trading-app-architecture-zh/) · 具体起点见[参考 MVP](/blog/flutter-trading-mvp-zh/)*

