大型报表每次接收新数据都完整重绘,CPU 与内存开销明显。差量更新的思路很自然:比较新旧数据,只把新增、删除和改变的部分交给渲染层。

收益来自减少工作,代价来自必须可靠地判断“什么变了”。

标识比比较更重要

没有稳定主键,只能按位置或完整对象比较。排序变化会被误认为所有行都改变,深比较本身也可能比重绘更贵。数据模型需要提供稳定标识和版本,更新算法才能保持简单。

我们先用映射建立旧数据索引,再生成新增、更新和删除集合。字段变化还要区分是否影响视觉,例如后台元数据变化不需要重绘图形。

一个 O(n) 的基础实现足以覆盖大多数表格和图元:

type Row = { id: string; version: number; value: number; label: string };
 
function diffRows(previous: Row[], next: Row[]) {
  const before = new Map(previous.map((row) => [row.id, row]));
  const added: Row[] = [];
  const updated: Row[] = [];
 
  for (const row of next) {
    const old = before.get(row.id);
    if (!old) added.push(row);
    else if (old.version !== row.version) updated.push(row);
    before.delete(row.id);
  }
 
  return { added, updated, removedIds: [...before.keys()] };
}

这里依赖服务端提供单调版本。如果只能深比较对象,首先要确认比较成本和数据语义;JSON.stringify 会受字段顺序影响,也会把和视觉无关的元数据变化算成更新。

什么时候放弃增量

变化比例很高、排序规则改变或图表配置变化时,完整重建反而更可靠。系统应设置阈值,在增量成本接近全量时切换策略,而不是执着于每次最小修改。

我会记录 changed / total、diff 耗时和渲染耗时,再决定阈值。假设 60% 数据变化时,增量路径既要维护索引,又要执行大量局部操作,往往不如一次全量替换。阈值不是固定经验值,而是当前渲染器和数据规模下的测量结果。

const changeRatio = (added.length + updated.length + removedIds.length) / next.length;
if (changeRatio > 0.45 || sortChanged || schemaChanged) {
  renderer.replaceAll(next);
} else {
  renderer.applyPatch({ added, updated, removedIds });
}

差量路径与全量路径必须得到一致结果。测试用随机变化序列同时执行两种策略,比较最终状态,可以发现遗漏删除、顺序错误和缓存失效问题。

这类测试适合属性测试:随机生成增加、删除、更新和重排序列,让增量状态与 replaceAll 后的状态做深比较。全量路径虽然慢,却是很好的参照实现;没有这条校准路径,增量缓存一旦漂移就很难自证。

先减少进入更新的数据

有时最有效的差量并不在 DOM 层。接口若能返回版本或变更游标,前端就不必先下载完整数据再比较;状态管理层若保留结构共享,组件也能快速判断哪些分支未变。越靠近数据源识别变化,后续各层浪费越少。

但端到端增量需要协议支持。丢失一段变更时必须能够重新同步全量,版本不连续要被发现。增量是一条快路径,全量仍是校准事实和恢复系统的基准路径。

性能优化总会交换复杂度。只有测量证明重绘是主要瓶颈,并且数据拥有稳定身份时,差量更新才是一笔值得做的交易。

客户端与版本、差量和全量存储之间的更新回退时序

图:任何版本缺口或 hash 不一致都必须回到可信基线。

差量协议必须守住三个不变量:同一 patch 重放结果不变;任何版本缺口都能回退全量;应用 patch 后的内容 hash 能与服务端目标 hash 对齐。客户端不满足任一项就停止继续增量,重新拉取基线。

上线时同时观察差量命中率、回退全量率、hash 不一致和节省字节。只看带宽下降,会忽略客户端状态逐渐偏离这个更危险的问题——增量路径的正确性只能由全量基线证明,这也是“全量路径必须保留”的原因。

在报表场景里,diff 之前还有一层:数据本身可以先按版本和变化游标交付,让前端不必下载完整快照再比较。这个思路与十万级数据渲染里按像素聚合、按需取数的预算思想一致——先减少进入系统的数据,再优化对已有数据的处理。