“我的电脑上拖起来不卡”不是性能结论。无限画布要用可复现数据证明:查询有界、对象有界、资源有界、长任务受控,而且公开发布没有泄漏私有实现。

学习目标

  • 建立从 Document 到 Renderer、资源和帧时间的指标链;
  • 设计规模、密度、媒体和交互四类基准场景;
  • 区分平均值、分位数、长任务与内存峰值;
  • 做故障注入验证恢复与降级;
  • 理解业务无关开源的泄漏、来源和包内容门禁。

1. 最少要观察哪些数字

指标 回答的问题
documentNodeCount 整幅作品有多大
spatialCandidateCount 本次查询缩小到多少候选
workingSetSize 当前要求活跃多少节点
renderedObjectCount 实际创建多少 handle
decodedBytes / objects 解码媒体占多少预算
activePlayers 有多少真实视频播放器
hydrateQueue / inFlight 资源准备是否形成积压
frameTime p50/p95/p99 交互是否稳定而非偶尔流畅
longTasks 主线程是否出现 >50ms 阻塞
historyBytes Undo 是否随着操作失控

四个最核心不变量:

Document 可以远大于 Working Set
Rendered Objects 稳定后接近 Working Set
Decoded Bytes 永不突破硬预算
一次用户/Agent 意图只产生一个原子 History Transaction

2. 不要只测一个节点分布

至少覆盖:

均匀分布:检验大 World 平移
局部高密度:检验视口内大量节点
长距离跳转:检验 enter/exit 峰值和取消
媒体画布:4K 图片、共享资产、视频 poster/player
编辑压力:连续拖动、缩放、Undo/Redo
Agent 批量:一次创建/排列大量节点

RBush 在均匀分布很快,不代表高度重叠的节点也一样;Working Set 有界也不代表某个超密集区域能保持同样对象数。

3. 一个可复现的容量基准

Viewport:1440 × 900,DPR 2
Document:1,200 / 12,000 / 50,000
Node distribution:固定 seed
Camera path:固定 30 秒平移 + 缩放 + 远区跳转
Warm-up:一次完整路径
Measure:重复 5 次,记录 p50/p95/p99

输出至少包含:

document / candidates / working set / handles
hydrates / releases / cancelled hydrates
decoded bytes peak / active players peak
frame time percentiles / long tasks / dropped frames

固定随机 seed 和 Camera path,才能比较不同提交、设备和 Renderer。

4. 性能预算来自产品,而不是框架口号

示例门槛:

decodedBytes <= 64 MiB
renderedObjects <= expected viewport density + pinned cap
activePlayers <= 2
inFlightHydrates <= 4
Camera interaction p95 <= 16.7ms(60Hz 目标)
no unbounded growth after 10 round trips

门槛应按目标设备分档。低端集显、远程桌面和高 DPR 需要不同预算。不要用开发机平均帧率替代目标环境。

5. 内存验收要看“回来没有”

一次峰值并不足以判定泄漏。执行往返:

初始视口
  → 远区 A
  → 远区 B
  → 回初始
  → 等待 exit grace + GC 观察窗口
  → 重复 10 次

检查 handle、lease、decoded objects、player 和 event listener 是否回到稳定区间。JS heap 可能因 GC 时机波动,但业务计数不应阶梯式增长。

6. 故障注入

需要主动制造:

  • 图片解码迟到、失败或被 Abort;
  • IndexedDB 配额不足、事务失败;
  • Renderer destroy 后异步任务返回;
  • WebGL/GPU context loss(若有 GPU Adapter);
  • Agent 超时、非法 JSON、超量 Command、revision conflict;
  • 页面刷新时 Document/History revision 不一致。

目标不是所有请求都成功,而是失败后 Document 真相、预算和可恢复性仍然正确。

7. 为什么 GPU 不是第一答案

无限画布是 Document/Camera/Working Set 的容量模型,不是某个 GPU 后端。换 WebGL/WebGPU 可能提高可见矢量的吞吐,却不会自动解决:

全量 Document 扫描
全量场景对象
图片解码内存
视频播放器数量
全量 History 快照

只有基准证明“可见内容绘制”是主要瓶颈时,才值得新增 GPU Adapter;Context Loss 后仍必须能从 Document 和 AssetStore rehydrate。

8. 开源门禁:不泄漏业务代码

guard:public
  检查私有绝对路径、内部域名、人员信息、凭据、token、跨仓 import

guard:origin
  对候选源码与私有参考树做归一化 token/结构比较
  发现高相似片段时要求人工解释或重写

guard:package
  检查最终 npm tarball,而不只检查 Git 工作区

release:check
  缺少必要私有参考根时失败关闭

课程可以讲公开的概念和算法,但不能复制业务节点、内部 API、品牌资源、真实数据、私有路径和凭据。抽象后的命名必须是 CanvasNodeAssetStoreRendererPort 等通用概念。

9. 为什么还要检查最终 tarball

Gitignore 不等于 npm ignore,构建产物也可能意外包含 source map、测试 fixture 或本地路径。发布前:

npm pack --dry-run
  → 查看文件白名单、大小、bin/exports
  → 解包扫描凭据、邮箱、绝对路径、内部域名
  → 在临时项目分别验证 ESM/CJS 与子路径 exports

API Key 和 npm token 只能来自本地凭据环境,不能提交到公开仓库、Docker image 或示例 .env

10. 公开课完整演示顺序

  1. Camera Demo:证明 World 不随视图移动;
  2. 1,200 节点 Demo:Document 远大于 Working Set;
  3. 快速平移:观察 enter/stay/exit 与 grace;
  4. Resource Demo:lease 阻止突破硬预算;
  5. IndexedDB 刷新:恢复 Document/History,但只 hydrate 当前区域;
  6. Agent Transaction:批量修改、冲突拒绝、整体 Undo;
  7. Renderer Switch:Canvas2D/Konva 共用同一 Core;
  8. 展示指标面板和门禁报告,而不是只展示动画。

这条演示链分别证明坐标稳定、容量有界、生命周期正确、资源有界、恢复正确、写入可控、框架可替换和发布安全。

11. 发布验收清单

[ ] Document 规模至少是 Working Set 的一个数量级
[ ] handles/decoded/player 在往返后回到稳定区间
[ ] 硬预算无法被活跃 lease 突破
[ ] History/Journal 有步数和字节预算
[ ] 失败注入不破坏 Document 真相
[ ] ESM/CJS/子路径 exports 在临时项目通过
[ ] 最终 tarball 无凭据、私有路径和业务标识
[ ] Docker 以非 root、只读文件系统和资源限制运行
[ ] 文档链接、双语构建和 source_hash 门禁通过

常见错误

  • 只看 FPS 平均值;
  • 只测初始视口,不测长距离往返;
  • 只看 JS heap,不看业务对象计数;
  • 用 GPU 替代 Working Set 与资源释放;
  • 把本地自动化通过说成真实设备发布验收;
  • 只扫描源码,不扫描 dist/tarball/Docker context;
  • 因为“概念相同”就复制私有业务实现。

课后实验

写一个固定 seed 的 30 秒 Camera 路径,输出 JSON 基准报告。分别将 overscan、exit grace、decoded budget 改为三组值,比较 p95 帧时间、hydrate 次数、峰值 bytes 和往返后的稳定对象数。最后解释哪个参数只是把成本从一处移动到另一处。