性能
在 97 项工作负载中与 Immer、手写 reducer 的性能对比,以及基准测试的运行方式。
在 97 项基准工作负载中,相同配置下 Mutative 的性能约为 Immer 的 3.6 倍,各自默认配置下约为 6.6 倍。在多数场景中,只复制变化部分的手写 reducer 仍然更快。
基准测试套件
基准测试套件测量 101 项工作负载的耗时:Immer 自身的性能测试、数组方法、稀疏数组、读取、Map 和 Set 的值、对象记录、类实例、深层路径、补丁应用、返回值和搜索。最近一次完整测试测量了其中 97 项,另一次专项测试测量了两个返回全新数据的场景;两个稀疏数组场景是在此后加入的。套件先用手写 reducer 验证每个结果,再比较 Mutative、Immer 11.1.18 和手写 reducer 的耗时。性能报告包含完整结果、测试方法及其局限。
Mutative 与 Immer 对比
Mutative 通过了 Immer 的全部测试用例。
在相同配置下(同时开启或关闭冻结,同时开启或关闭补丁),Mutative 在 614 个测量案例中的 595 个更快,耗时比的几何平均值为 3.6 倍。各自默认配置下(Mutative 关闭自动冻结,Immer 开启),Mutative 在 157 个案例中的 154 个更快,几何平均值为 6.6 倍。其余 3 个场景返回的是 Immer 此前已冻结的状态:Immer 搜索返回状态中的草稿时会跳过已冻结的值,而默认不冻结的 Mutative 会继续搜索,除非用 rawReturn() 包裹返回值;此时耗时为 0.26 µs。
整体性能对比
97 项工作负载 · Immer / Mutative 耗时比的几何平均值
相同配置
自动冻结与补丁设置一致
3.6×几何平均加速比
595 / 614 个案例中 Mutative 更快
- Mutative 更快: 595
- 差距在 5% 以内: 13
- Immer 更快: 6
各自默认配置
仅 Immer 自动冻结,均关闭补丁
6.6×几何平均加速比
154 / 157 个案例中 Mutative 更快
- Mutative 更快: 154
- 差距在 5% 以内: 0
- Immer 更快: 3
下表单位为每次更新的微秒数,取 Apple M1 Max、Node.js 24.16.0 环境下三次测试的中位数,各库在独立进程中运行;数值越小越好。Mutative、第一列 Immer 和手写 reducer 均关闭自动冻结,第二列 Immer 使用其默认配置。
| 工作负载 | 行数 | Mutative | Immer | Immer,自动冻结 | 手写实现 |
|---|---|---|---|---|---|
| 更新小对象的一个字段 | — | 0.51 | 0.69 | 0.95 | 0.02 |
| 按 ID 找到并更新数组项 | 100 | 1.12 | 1.82 | 25.0 | 0.38 |
| 200 次 RTK Query 风格更新 | — | 1,001 | 1,133 | 4,795 | 352 |
| 按索引读取每一行 | 10,000 | 4,756 | 9,584 | 9,687 | 57.7 |
| 删除第一行 | 10,000 | 7.04 | 7,408 | 8,137 | 1.40 |
| 更新每一行 | 10,000 | 6,462 | 12,570 | 15,607 | 211 |
| 更新 10,000 条记录中的一条 | 10,000 | 1,200 | 2,094 | 3,131 | 2,088 |
| 向含 1,000 个属性的对象插入属性 | — | 50.3 | 149 | 229 | 126 |
| 更新 Map 中的值 | 10,000 | 518 | 526 | 733 | 519 |
| 向 Set 添加一个数字 | 10,000 | 29.5 | 904 | 975 | 61.2 |
| 更新类实例 | 10,000 | 2.92 | 3.44 | 668 | 1.42 |
| 更新十层嵌套的值 | — | 3.50 | 4.41 | 7.84 | 0.76 |
| 对 10% 的行应用补丁 | 10,000 | 734 | 1,166 | 2,095 | — |
shift() 后搜索 current() | 10,000 | 257 | 11,255 | 11,846 | 64.8 |
| 返回 draft.filter() | 10,000 | 2,658 | 4,251 | 4,736 | 77.7 |
| 返回新状态 | 10,000 | 2,604 | 5,718 | 0.79 | 0.02 |
| 通过 rawReturn() 返回 | 10,000 | 0.26 | 5,703 | 0.79 | 0.02 |
每个库的每项工作负载均在独立进程中运行。把所有库放在同一进程时,所有库都会变慢,且幅度不同:Mutative 耗时的几何平均值为独立进程时的 1.1 倍,Immer 为 1.2 倍,手写 reducer 为 1.7 倍。搜索场景先用 shift() 删除第一行,再在数组快照中找到最后一行并更新。Immer 没有 rawReturn(),因此最后两行均返回相同的普通值;开启自动冻结后,Mutative 也像 Immer 一样跳过已冻结的返回状态,耗时为 0.53 µs。在 614 个相同配置的案例中,Immer 在 6 个案例中更快,全部开启了自动冻结:其中 4 个更新含 100 至 10,000 个字段的类实例,另 2 个更新 100 行数组,其多次运行的耗时区间与 Mutative 重叠。
大数组
当规模为 1,000 和 10,000 行时,差距进一步扩大。与未开启数组方法插件的 Immer 相比,Mutative 在这些规模的 232 个案例中的 222 个更快,几何平均值为 4.7 倍,且在所有移动元素的案例中都更快:
性能差距如何随数组规模变化
Immer 耗时 / Mutative 耗时 · 越高越有利于 Mutative
冻结 关闭 · 补丁 关闭
8.7× → 10.8× 规模从 1 千行 → 1 万行
冻结 关闭 · 补丁 开启
4.1× → 4.5× 规模从 1 千行 → 1 万行
冻结 开启 · 补丁 关闭
3.9× → 4.2× 规模从 1 千行 → 1 万行
冻结 开启 · 补丁 开启
2.7× → 2.8× 规模从 1 千行 → 1 万行
| 自动冻结 | 补丁 | 全部场景,1,000 行 | 全部场景,10,000 行 | 元素移动,1,000 行 | 元素移动,10,000 行 |
|---|---|---|---|---|---|
| 关闭 | 关闭 | 8.7x | 10.8x | 209x | 430x |
| 关闭 | 开启 | 4.1x | 4.5x | 7.1x | 7.3x |
| 开启 | 关闭 | 3.9x | 4.2x | 32x | 39x |
| 开启 | 开启 | 2.7x | 2.8x | 6.6x | 6.8x |
每个值都是 Immer 耗时除以 Mutative 耗时的几何平均值;元素移动包括 shift、unshift、splice 插入和 reverse。Mutative 在副本上原生移动元素,Immer 则逐个通过草稿代理移动:删除 10,000 行中的第一行分别需要 7.04 µs 和 7,408 µs,相差 1,052 倍。开启补丁后,两者都为每个移动的索引生成补丁,因此优势缩小为 5–9 倍。性能报告按场景列出了详细结果。
开启补丁
开启补丁、关闭自动冻结时,Mutative 在 150 个案例中的 148 个快于 Immer,其余 2 个差距在 5% 以内,几何平均值为 3.3 倍;相比 Mutative 1.3.0,在 131 个案例中更快,其余差距在 5% 以内,几何平均值为 4.6 倍。在所有已测量的数组场景中,它都快于两者:几何平均值分别为 Immer 的 4.4 倍和 Mutative 1.3.0 的 13 倍。如果更新只涉及少量路径,补丁开销很小:在 10,000 行场景中追加一行并插入一个属性,开启补丁耗时 63.2 µs,关闭时为 63.8 µs。移动元素时,各库都会为每个移动索引生成补丁,因此删除 10,000 行中的第一行会生成 10,000 个正向补丁和 10,000 个逆向补丁。
以下耗时单位为每次更新的微秒数,开启补丁、关闭自动冻结:
生成补丁时的更新成本
每次更新耗时(微秒)· 越短越好
追加一行并插入一个属性
性能为 Immer 的 5.7×
用 shift 删除第一行
性能为 Immer 的 8.41×
更新每一行
性能为 Immer 的 2.54×
更新 10,000 条记录中的一条
性能为 Immer 的 1.78×
| 工作负载 | 行数 | Mutative | Immer | Mutative 1.3.0 |
|---|---|---|---|---|
| 追加一行并插入一个属性 | 10,000 | 63.2 | 360 | 210 |
| 按 ID 找到并更新数组项 | 100 | 1.50 | 3.22 | 3.35 |
| 200 次 RTK Query 风格更新 | — | 1,140 | 1,470 | 2,116 |
用 splice 删除第一行 | 100 | 11.5 | 83.6 | 2,982 |
| 在中间插入一行 | 100 | 8.83 | 48.0 | 1,191 |
| 对行排序 | 100 | 105 | 129 | 2,466 |
用 shift 删除第一行 | 10,000 | 1,063 | 8,943 | 265,439 |
| 反转行顺序 | 10,000 | 1,071 | 9,424 | 280,542 |
| 更新每一行 | 10,000 | 8,555 | 21,767 | 22,549 |
| 更新 10,000 条记录中的一条 | 10,000 | 1,184 | 2,104 | 1,181 |
Immer 可选的 enableArrayMethods() 插件也在草稿副本上执行数组方法。开启补丁时,移动 1,000 或 10,000 行,Immer 的耗时为 Mutative 的 1.13–1.70 倍(即慢 13%–70%)。本对比未开启该插件,因为在 Immer 11.1.18 中它破坏了通常成立的保证:shift、pop 和 splice 返回原始基础对象,修改移出的对象会改变之前的状态;重排序也可能暴露原始对象,并在结果中留下已撤销的草稿;正向补丁还可能无法正确重放。审计中的 4,913 组三步操作序列,有 123 组在开启插件时出错,关闭时则全部通过。性能报告单独列出了开启该插件的 Immer 结果。
Mutative 与手写 reducer 对比
只复制变化部分的手写 reducer 比使用草稿机制的库更快,在套件的 150 个案例中有 129 个快于 Mutative。Mutative 的优势在于:复制宽对象(如含 1,000 个键的记录或含 100 个字段的类实例,快 1.5–2.8 倍)、在 10,000 行数组头部插入(16 µs 对 28 µs),以及向含 1,000 或 10,000 个数字的 Set 添加数字。Mutative 复制 Set 比 new Set() 更快,在 10,000 个元素时为 30 µs 对 61 µs。
如果代码反复复制同一结构,展开语法也会变慢。无论更新函数修改多少次,Mutative 每次更新最多复制每个对象一次。
如何避免不必要的草稿、搜索、复制、冻结和补丁,见性能建议。
运行基准测试
克隆仓库后运行:
pnpm install --frozen-lockfile
pnpm benchmark:immer:check
pnpm benchmark:immer基准测试指南介绍了工作负载、选项、内存测量和 CI 回归预算。
最后更新于 作者:unadlib