几年前——按现在的标准感觉像上古历史了——Twitter上吵得火热的是Pretext.js,一个文本布局库。它的 headline 声称比标准 DOM 测量快得多:不让浏览器布局引擎计算文字尺寸,而是在纯 JavaScript 里完成等价工作,数据通过 prepare() 一次性准备好。理论上,每次测量都不会触发强制重排。

不同人的演示看起来都很 impressive,我好奇实际到底有多快。只有一个问题:我并不擅长性能测量。除了基础的 Core Web Vitals 工作——LCP、INP 这些常规指标——我对这种微基准测试毫无经验。

打开网易新闻 查看精彩图片

我开始研究这些测量到底怎么做,结论是应该从最简单的测试开始:比较三种策略的单次调用成本——DOM、Canvas、Pretext。

我选了最精简的配置,避免测试框架本身拖累运行时:Astro 静态输出,只在需要交互的地方用 React 孤岛,手写测量工具。我为每种策略写了最小实现,开始跑测试。

以下是我学到的东西。几乎都和 Pretext 无关。

第一轮基准测试结果长这样:大部分格子显示零。不是零的那些 suspiciously 整齐——正好 100.00、200.00、400.00。这些整数是线索。

2018 年 Spectre 漏洞披露后,浏览器故意降低 performance.now() 的精度,让基于计时的内存攻击更难。Chrome 把计时器四舍五入到约 100 微秒,Firefox 和 Safari 约 1 毫秒。你可以在控制台验证:

console.log(performance.now() % 1)
// Chrome: ~0.1 递增
// Firefox: 0

我一直在基准测试某些情况下不到 1 微秒就完成的操作。当然返回零——计时器根本没有更小的单位来表达。

crossOriginIsolated 页面可以通过基于 SharedArrayBuffer 的时钟解锁约 5 微秒计时精度,但设置 COOP/COEP 响应头很麻烦。批量计时在不碰响应头的情况下达到同样的有效精度。

正经基准测试库(benchmark.js、tinybench、criterion 风格框架)的标准修复方案是批量计时。不是每次调用单独测,而是测 N 次调用的批次再除以 N:

// 我之前写的——有问题
for (let i = 0; i < 1000; i++) {
const t0 = performance.now()
measure()
timings.push(performance.now() - t0)
}

// 应该是这样
const BATCH = 1000
for (let i = 0; i < iterations; i++) {
const t0 = performance.now()
for (let j = 0; j < BATCH; j++) measure()
timings.push((performance.now() - t0) / BATCH)
}