DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Immer 关闭自动冻结后,大对象赋值仍会遍历

Immer 11 关闭自动冻结仍可能遍历新赋入的大对象,以完成内部 draft 引用处理。确认数据不含 draft 且不再外部修改时预冻结,保留正确性与数据所有权验收。

Immer 从 10.x 升到 11.x,已经关闭 auto-freeze,给 draft 赋入一份大列表仍明显变慢。若只给根字段赋值较快,再增加一次嵌套修改就出现额外开销,应检查新赋值内部是否需要继续查找 draft。

关闭自动冻结,只表示不为最终结果执行那项冻结工作,不表示可以跳过所有遍历。Immer 11 修正了新对象内部可能嵌有 draft 的正确性问题;11.2.9 的正式实现仍会在相应分支处理这些引用。这里不能用“freeze 已关闭”推导“赋值应恒定耗时”。

确认数据确实不含 draft,再预先冻结

应用自己新建、确认没有 draft 引用的数据,可以在进入 produce 前冻结根对象,告诉 Immer 无需再进入这棵数据。下面用独立 Immer 实例说明,不修改其他模块的全局设置:

import { Immer, freeze } from 'immer';

const localImmer = new Immer({ autoFreeze: false });
const base = {
  byRoom: { room: { list: [] } },
  meta: { loading: true },
};
const list = freeze([
  { id: 1, details: { name: 'a' } },
]);

const next = localImmer.produce(base, (draft) => {
  draft.byRoom.room = { list };
  draft.meta.loading = false;
});

freeze 默认是浅冻结。这里的 list 来自函数外新建的普通数据,不包含 produce 中的 draft,且交给状态以后不再从其他引用修改;这些条件才使跳过遍历有意义。不要把某次 recipe 里的 draft 塞进对象,再 freeze 外壳,借此阻止 Immer 完成内部引用。

浅冻结不会让 details 自动不可变。项目需要深度不可变保证时,应按整体策略处理;不能为了一个性能分支关闭原有防护,或在状态外继续修改共享的子对象。

先定位赋值形态,再比较性能

相关路径会检查 scope 中的 draft 数量等状态,因此“顶层赋值”和“顶层赋值加一处嵌套写入”不一定有相同成本。只有实际 recipe 形态接近,计时对比才有意义。

保留数据层级、条目数量、auto-freeze 设置、实际版本及同一次 recipe 的其他写入,分别观察预冻结前后的变化。不要拿别人的毫秒数作为自己应用的性能承诺,也不要只计 produce 而漏掉移到前面的深冻结成本。

这不同于constructor 代理触发 Lodash 错误。那篇需要处理特定版本的 Proxy 约束,本题的遍历还承担清理内部 draft 的正确性职责,不能删除它或回退旧版本后宣称同样安全。

预冻结会改变外部数据的可写性。调整前保留原数据所有权与冻结策略,撤回时恢复原构造和赋值路径,并检查持有同一对象的其他代码。验收除耗时外,还需确认输出、原状态未修改、引用共享与冻结范围都符合既有约定。