Performance API:`performance.now()` 的高精度时间戳与浏览器时间源

各位听众,大家下午好。

今天,我们将深入探讨Web前端性能优化中一个至关重要的工具——Performance API 中的 performance.now() 方法。这个方法提供的高精度时间戳,对于我们精确测量代码执行时间、优化用户体验、调试复杂交互等方面,具有不可替代的价值。我们将从其基本概念出发,逐步揭示其背后的浏览器时间源机制,探讨它在各种场景下的实际应用,并讨论其高级特性、安全考量以及未来的发展。

一、引言:为什么我们需要高精度时间戳?

在Web开发的早期,我们获取时间戳的常见方式是使用 Date.now()。它返回自Unix纪元(1970年1月1日00:00:00 UTC)以来的毫秒数。对于大多数日常任务,例如记录日志、显示当前时间或者计算两个事件之间的大致间隔,Date.now() 已经足够了。

然而,随着Web应用日益复杂,用户对性能和流畅度的要求也越来越高。当我们需要测量微小的性能瓶颈、精确同步动画、或者分析复杂的用户交互延迟时,Date.now() 的局限性就暴露无遗了:

  1. 精度不足: Date.now() 的精度通常是毫秒级,这意味着它无法区分在同一毫秒内发生的多个事件。对于需要亚毫秒(微秒甚至纳秒)精度的场景,例如测量一个短函数执行时间、计算渲染帧率或者评估用户输入延迟(FID – First Input Delay),毫秒级的精度是远远不够的。
  2. 非单调性: Date.now() 是基于系统时钟的。如果用户手动更改了系统时间,或者系统通过NTP(网络时间协议)自动同步时间,Date.now() 返回的值可能会“跳跃”,甚至倒退。这会使得我们无法可靠地计算时间间隔,因为 endTime - startTime 可能会得到一个负值或异常大的值。
  3. 基准点问题: Date.now() 的基准点是Unix纪元,这是一个绝对时间。而对于性能测量,我们更关心的是页面加载或某个特定事件发生后的相对时间,而不是绝对的日历时间。

为了解决这些问题,Web标准引入了 Performance API,其中最核心的成员之一便是 performance.now()。它旨在提供一个高精度、单调递增且与系统时间无关的时间戳,从而为前端性能优化提供强大的基础。

二、performance.now() 的基础概念与特性

performance.now() 方法返回一个 DOMHighResTimeStamp 类型的浮点数,表示自 timeOrigin 以来经过的毫秒数。这里的 timeOrigin 是一个非常重要的概念,它通常指的是当前文档或 worker 的生命周期开始的时间点。

2.1 定义与语法

performance.now() 返回的是一个 double 类型的浮点数,其精度可以达到微秒甚至更高(具体取决于浏览器和操作系统)。

const startTime = performance.now();
// 执行一些耗时操作
for (let i = 0; i < 1000000; i++) {
  Math.sqrt(i);
}
const endTime = performance.now();

console.log(`操作耗时: ${(endTime - startTime).toFixed(3)} 毫秒`);
// 示例输出: 操作耗时: 2.145 毫秒

2.2 与 Date.now() 的关键区别

为了更好地理解 performance.now() 的价值,我们通过一个表格来对比它与 Date.now() 的核心差异:

特性 performance.now() Date.now()
精度 高精度,通常可达微秒甚至更高(浮点数) 毫秒级(整数)
基准点 当前文档或 worker 的生命周期开始时间 (timeOrigin) Unix纪元(1970年1月1日00:00:00 UTC)
单调性 保证单调递增,不受系统时钟影响 非单调,可能因系统时钟调整而跳跃、倒退
返回类型 DOMHighResTimeStamp (浮点数) number (整数)
适用场景 性能测量、动画同步、游戏循环、事件调度等 记录绝对时间、日志、不要求高精度的计时

基准点 timeOrigin 的深入理解:

performance.now() 返回的时间戳并非从Unix纪元开始计算,而是相对于 performance.timeOrigintimeOrigin 通常代表以下时间点之一:

  • 对于主文档 (window.performance): 通常是浏览器开始加载当前文档的时间点。这包括了重定向、unload 事件等,但具体定义可能因浏览器而异。你可以通过 performance.timeOrigin 属性来获取这个值,它同样是一个 DOMHighResTimeStamp,表示从Unix纪元到 timeOrigin 的毫秒数。
  • 对于 Web Worker: 通常是 Worker 脚本开始执行的时间点。

这意味着,performance.now() 测量的是相对时间,这对于计算页面加载后的各种操作耗时更为直观和有用。例如,如果你想知道一个脚本从页面加载开始到执行完毕用了多长时间,你只需要在脚本的开始和结束分别调用 performance.now() 即可,而无需关心绝对的日历时间。

console.log(`performance.timeOrigin (Unix纪元到页面加载开始): ${performance.timeOrigin.toFixed(3)} 毫秒`);
console.log(`performance.now() (页面加载开始到现在): ${performance.now().toFixed(3)} 毫秒`);
console.log(`Date.now() (Unix纪元到现在): ${Date.now()} 毫秒`);

// 它们之间的关系大致是:Date.now() ≈ performance.timeOrigin + performance.now()
// 但由于精度和基准点定义略有差异,这只是一个近似关系。

2.3 单调递增性

performance.now() 最重要的特性之一是其 单调递增性。这意味着它返回的值总是大于或等于前一个调用返回的值,永远不会减小。即使系统时钟被调整(向前或向后),performance.now() 也不会受到影响,它将继续以稳定的速率递增。

这种单调性对于精确测量时间间隔至关重要。例如,如果你在 t1t2 时刻分别调用 performance.now(),那么 t2 - t1 总是能可靠地表示这两个时刻之间经过的时间,而不会因为系统时间调整而出现负值或不合理的结果。

2.4 时间源

performance.now() 背后依赖的是浏览器内部的高精度时钟,这个时钟通常与操作系统的单调时钟功能紧密集成。它不会受到网络时间同步或用户手动修改系统时间的影响,保证了时间戳的稳定性和可靠性。

三、浏览器时间源:如何实现高精度?

performance.now() 之所以能提供高精度且单调递增的时间戳,得益于底层操作系统和硬件的支持,并由浏览器进行了精巧的封装。

3.1 操作系统层面的支持

现代操作系统都提供了高精度的单调时钟接口,这些接口是 performance.now() 实现的基础:

  • Windows: Windows 系统提供了 QueryPerformanceCounterQueryPerformanceFrequency 函数。QueryPerformanceCounter 返回一个高分辨率的计数器值,而 QueryPerformanceFrequency 返回该计数器每秒的频率。通过这两个函数,可以计算出非常精确的时间间隔。这个计数器是系统启动以来一直递增的,不受系统时间调整影响。
  • Linux/macOS: 类Unix系统(如Linux和macOS)通常使用 clock_gettime 函数,并结合 CLOCK_MONOTONICCLOCK_MONOTONIC_RAW 时钟ID来获取单调递增的时间。
    • CLOCK_MONOTONIC:表示自系统启动以来经过的时间,不受系统时间调整影响。
    • CLOCK_MONOTONIC_RAW:类似于 CLOCK_MONOTONIC,但它不受NTP等机制调整的影响,直接反映硬件时钟的原始滴答。
  • Android/iOS: 移动操作系统也有类似的单调高精度时钟API,例如 Android 的 System.nanoTime() 和 iOS 的 mach_absolute_time()

这些操作系统级别的API能够利用硬件计时器,提供纳秒甚至更高的精度。

3.2 硬件层面的支持

在硬件层面,CPU中通常包含一个 TSC (Timestamp Counter) 寄存器。这个寄存器在每个CPU时钟周期都会递增,提供了极高精度的时间测量能力。操作系统的高精度时钟API往往会利用TSC或其他类似的硬件计时器来获取时间。

不过,直接使用TSC会面临一些挑战,例如多核CPU之间TSC可能不同步、CPU变频可能导致TSC频率变化等。因此,操作系统和浏览器会进行复杂的校准和抽象,以提供一个稳定、可靠且统一的高精度时间源。

3.3 浏览器如何封装

浏览器引擎(如Chromium的Blink、Firefox的Gecko、WebKit等)内部会调用操作系统提供的上述高精度单调时钟API。它们将这些底层API的原始时间数据进行处理和封装,最终通过 performance.now() 方法暴露给JavaScript环境。

这种封装确保了:

  1. 平台无关性: 开发者无需关心底层操作系统的差异。
  2. 安全性: 浏览器可以控制时间戳的暴露精度,以缓解潜在的安全风险(详见下文)。
  3. 标准化: 遵循Web标准,提供统一的API接口。

3.4 安全与隐私考虑

高精度计时器虽然强大,但也带来了一些安全和隐私风险,主要是 计时攻击 (Timing Attacks)

计时攻击: 恶意网站可以通过精确测量JavaScript代码的执行时间,来推断出一些敏感信息,例如:

  • 用户是否登录了某个网站: 测量访问受保护资源所需的时间,如果用户已登录,响应时间可能更快。
  • CPU缓存状态: 测量访问不同内存地址所需的时间,推断CPU缓存中的数据,进而可能泄露其他进程或同源网站的敏感信息。
  • 侧信道攻击 (Side-channel attacks) 的基础: 例如,在 Spectre 和 Meltdown 等CPU漏洞被发现后,高精度计时器被认为是进行侧信道攻击的关键工具之一。攻击者可以通过测量内存访问时间来推断出被隔离的敏感数据。

为了缓解这些风险,浏览器厂商采取了一些策略来限制 performance.now() 的精度:

  1. 精度限制 (Precision Throttling):
    • 在某些情况下(例如,页面在后台运行、跨域Iframe中),浏览器可能会动态降低 performance.now() 的精度,将其舍入到最近的整数毫秒或更高的值(例如,100微秒)。
    • Chrome、Firefox 等浏览器在没有启用 SharedArrayBufferWebAssembly.Memory 的情况下,会将 performance.now() 的精度限制在 100 微秒 (0.1 毫秒) 左右。
    • 这意味着,即使底层硬件和操作系统能够提供纳秒级精度,JavaScript 代码也可能只能获得较低精度的结果。
  2. 跨域隔离 (Cross-origin Isolation):
    为了重新启用 SharedArrayBuffer 和高精度计时器(包括 performance.now() 的全精度),网站需要启用 跨域隔离。这通常通过设置两个HTTP响应头来实现:

    • Cross-Origin-Opener-Policy: same-origin (COOP)
    • Cross-Origin-Embedder-Policy: require-corp (COEP)
      当页面处于跨域隔离状态时,它会确保自身不会加载任何未经COEP授权的跨域资源,并且不会与非跨域隔离的文档共享浏览上下文。这种严格的隔离环境减少了计时攻击的威胁,因此浏览器可以放心地提供高精度计时器。

总结一下: 如果你的网站需要 performance.now() 的最高精度(微秒级),你可能需要考虑配置 COOPCOEP 来启用跨域隔离。否则,在默认情况下,你获得的精度可能会受到限制。

四、performance.now() 的实际应用场景

performance.now() 的高精度和单调性使其在前端开发的多个领域都大放异彩。

4.1 性能测量与基准测试

这是 performance.now() 最直接和广泛的应用。

4.1.1 测量函数执行时间

我们可以精确测量任何JavaScript函数的执行耗时。

function heavyComputation() {
  let sum = 0;
  for (let i = 0; i < 10000000; i++) {
    sum += Math.sqrt(i) * Math.log(i + 1);
  }
  return sum;
}

const start = performance.now();
const result = heavyComputation();
const end = performance.now();

console.log(`heavyComputation 返回值: ${result}`);
console.log(`heavyComputation 执行耗时: ${(end - start).toFixed(4)} 毫秒`);

// 示例输出:
// heavyComputation 返回值: 1.094572236894564e+10
// heavyComputation 执行耗时: 37.1234 毫秒

4.1.2 测量动画帧率 (FPS)

requestAnimationFrame 回调中使用 performance.now() 可以精确计算两帧之间的时间间隔,进而推算出动画的帧率。

let lastFrameTime = performance.now();
let frameCount = 0;
const fpsMonitor = document.createElement('div');
fpsMonitor.style.cssText = 'position:fixed;top:10px;left:10px;background:rgba(0,0,0,0.7);color:white;padding:5px;font-family:monospace;z-index:9999;';
document.body.appendChild(fpsMonitor);

function animate(currentTime) {
  const deltaTime = currentTime - lastFrameTime; // currentTime 是 requestAnimationFrame 提供的 DOMHighResTimeStamp
  lastFrameTime = currentTime;

  frameCount++;
  if (frameCount % 60 === 0) { // 每60帧更新一次FPS显示
    const fps = (1000 / deltaTime).toFixed(1); // 1000ms / 帧间隔 = FPS
    fpsMonitor.textContent = `FPS: ${fps} (Avg: ${(60000 / (currentTime - performance.timeOrigin)).toFixed(1)})`;
  }

  // 渲染逻辑...
  // 例如,移动一个元素
  const element = document.getElementById('myElement');
  if (element) {
    element.style.transform = `translateX(${(currentTime / 10) % window.innerWidth}px)`;
  }

  requestAnimationFrame(animate);
}

// 确保页面有一个元素供动画演示
document.addEventListener('DOMContentLoaded', () => {
  const demoElement = document.createElement('div');
  demoElement.id = 'myElement';
  demoElement.style.cssText = 'width:50px;height:50px;background:blue;position:absolute;top:100px;';
  document.body.appendChild(demoElement);
  requestAnimationFrame(animate);
});

在这个例子中,requestAnimationFrame 回调函数接收的 currentTime 参数本身就是一个 DOMHighResTimeStamp,可以直接用于高精度计时。

4.1.3 测量资源加载时间

虽然 PerformanceResourceTiming 提供了更全面的资源加载时间数据,但对于特定场景,performance.now() 也可以辅助测量。

// 假设我们要测量一张图片的加载时间
const img = new Image();
const imgLoadStartTime = performance.now();

img.onload = () => {
  const imgLoadEndTime = performance.now();
  console.log(`图片加载完成! 耗时: ${(imgLoadEndTime - imgLoadStartTime).toFixed(3)} 毫秒`);
};

img.onerror = () => {
  console.error('图片加载失败!');
};

img.src = 'https://via.placeholder.com/150'; // 替换为你的图片URL

4.1.4 测量用户交互延迟 (FID 的基础)

FID (First Input Delay) 是一个重要的Web Vitals指标,衡量用户首次与页面交互(点击、输入等)到浏览器实际开始处理这些交互之间的时间。performance.now() 是计算这类延迟的基础。

例如,测量从用户点击到事件处理器开始执行的时间:

document.addEventListener('click', (event) => {
  // event.timeStamp 属性本身也是一个 DOMHighResTimeStamp,
  // 表示事件发生的时间(相对于 timeOrigin)。
  // 它可以用于衡量事件到处理开始之间的延迟。
  const clickStartProcessingTime = performance.now();
  const delay = clickStartProcessingTime - event.timeStamp;
  console.log(`点击事件处理开始延迟: ${delay.toFixed(3)} 毫秒`);

  // 模拟一些UI阻塞操作
  for (let i = 0; i < 1000000; i++) {
    Math.sin(i);
  }
  console.log('点击事件处理完成');
});

// 注意:event.timeStamp 的精度和来源可能与 performance.now() 略有不同,
// 但它们都属于 DOMHighResTimeStamp 范畴,用于高精度计时。
// 对于 FID,更准确的测量可能需要结合 PerformanceObserver API。

4.2 游戏开发与动画同步

在游戏和交互式动画中,精确的时间控制至关重要。

4.2.1 物理引擎步进

物理引擎通常需要以固定的时间步长进行更新,以确保模拟的稳定性和可预测性。performance.now() 可以帮助我们计算实际经过的时间,并调整物理步进的次数。

const FIXED_UPDATE_INTERVAL = 1000 / 60; // 60 FPS
let lastUpdateTime = performance.now();
let accumulator = 0; // 用于累积未处理的时间

function gameLoop(currentTime) {
  const deltaTime = currentTime - lastUpdateTime;
  lastUpdateTime = currentTime;
  accumulator += deltaTime;

  // 以固定时间步长更新物理
  while (accumulator >= FIXED_UPDATE_INTERVAL) {
    updatePhysics(FIXED_UPDATE_INTERVAL); // 每次更新都传递固定时间步长
    accumulator -= FIXED_UPDATE_INTERVAL;
  }

  render(accumulator / FIXED_UPDATE_INTERVAL); // 渲染时插值以平滑动画

  requestAnimationFrame(gameLoop);
}

function updatePhysics(step) {
  // 模拟物理更新
  // console.log(`物理更新步进: ${step.toFixed(2)}ms`);
}

function render(alpha) {
  // 渲染当前帧,可以使用 alpha 进行插值
  // console.log(`渲染帧,插值因子: ${alpha.toFixed(2)}`);
}

requestAnimationFrame(gameLoop);

这个“固定时间步长”模式是游戏开发中的常见做法,它将物理更新与渲染解耦,确保物理模拟的确定性,同时使用 performance.now() 来精确计算并累积实际经过的时间。

4.2.2 动画插值

基于时间的动画,需要根据经过的时间来计算动画的当前状态。

const duration = 2000; // 动画总时长 2 秒
let animationStartTime = null;
const box = document.createElement('div');
box.style.cssText = 'width:100px;height:100px;background:red;position:absolute;top:200px;left:0;';
document.body.appendChild(box);

function animateBox(currentTime) {
  if (!animationStartTime) {
    animationStartTime = currentTime;
  }

  const elapsed = currentTime - animationStartTime;
  const progress = Math.min(elapsed / duration, 1); // 0 到 1 的进度

  // 使用缓动函数,例如线性插值
  const newLeft = progress * (window.innerWidth - 100);
  box.style.left = `${newLeft}px`;

  if (progress < 1) {
    requestAnimationFrame(animateBox);
  } else {
    console.log('动画完成!');
    animationStartTime = null; // 重置以便下次触发
  }
}

// 点击页面开始动画
document.addEventListener('click', () => {
  if (!animationStartTime) {
    requestAnimationFrame(animateBox);
  }
});

4.3 事件循环与任务调度

performance.now() 可以帮助我们分析JavaScript事件循环中的任务执行顺序和耗时,从而找出潜在的UI阻塞点。

console.log(`[${performance.now().toFixed(3)}ms] 主线程任务开始`);

setTimeout(() => {
  console.log(`[${performance.now().toFixed(3)}ms] setTimeout 任务 1 执行`);
}, 0);

Promise.resolve().then(() => {
  console.log(`[${performance.now().toFixed(3)}ms] Promise Microtask 1 执行`);
});

setTimeout(() => {
  console.log(`[${performance.now().toFixed(3)}ms] setTimeout 任务 2 执行`);
  Promise.resolve().then(() => {
    console.log(`[${performance.now().toFixed(3)}ms] Promise Microtask 2 (在 setTimeout 后) 执行`);
  });
}, 0);

// 模拟一个长耗时的主线程任务
const heavyTaskStartTime = performance.now();
for (let i = 0; i < 5000000; i++) {
  Math.tan(i);
}
const heavyTaskEndTime = performance.now();
console.log(`[${heavyTaskEndTime.toFixed(3)}ms] 模拟耗时主线程任务完成,耗时: ${(heavyTaskEndTime - heavyTaskStartTime).toFixed(3)}ms`);

console.log(`[${performance.now().toFixed(3)}ms] 主线程任务结束`);

// 预期输出顺序(时间戳会体现延迟):
// [X.XXXms] 主线程任务开始
// [Y.YYYms] 模拟耗时主线程任务完成,耗时: Z.ZZZms
// [A.AAAms] 主线程任务结束
// [B.BBBms] Promise Microtask 1 执行 (在主线程任务结束后立即执行)
// [C.CCCms] setTimeout 任务 1 执行 (在所有微任务之后,下一个宏任务队列)
// [D.DDDms] setTimeout 任务 2 执行
// [E.EEEms] Promise Microtask 2 (在 setTimeout 后) 执行 (在 setTimeout 2 宏任务结束后立即执行)

通过观察时间戳,我们可以清晰地看到微任务(Promise)总是优先于宏任务(setTimeout)执行,并且耗时的主线程任务会阻塞后续所有任务的执行。

4.4 Web Audio/Video 同步

在处理音频和视频时,精确同步音频播放和视觉内容是关键。Web Audio API 自身提供了高精度的 AudioContext.currentTime,但如果需要将音频事件与 requestAnimationFrame 驱动的视觉动画同步,performance.now() 可以作为连接两者的时间桥梁。

例如,在播放音频的同时,让一个进度条精确地跟随音频的播放:

const audio = new Audio('your_audio_file.mp3'); // 替换为你的音频文件
const progressBar = document.createElement('div');
progressBar.style.cssText = 'width:0;height:10px;background:green;position:absolute;bottom:20px;left:0;';
document.body.appendChild(progressBar);

let audioPlayStartTime = null;

audio.addEventListener('play', () => {
  audioPlayStartTime = performance.now();
  requestAnimationFrame(updateProgressBar);
});

audio.addEventListener('pause', () => {
  audioPlayStartTime = null;
});

audio.addEventListener('ended', () => {
  audioPlayStartTime = null;
  progressBar.style.width = '100%'; // 确保最终进度条满
});

function updateProgressBar(currentTime) {
  if (audioPlayStartTime === null || audio.paused || audio.ended) {
    return;
  }

  const elapsedInMs = currentTime - audioPlayStartTime;
  const progress = elapsedInMs / (audio.duration * 1000); // audio.duration 是秒,转换为毫秒
  progressBar.style.width = `${Math.min(progress * 100, 100)}%`;

  if (progress < 1) {
    requestAnimationFrame(updateProgressBar);
  }
}

// 启动音频播放
document.addEventListener('click', () => {
  if (audio.paused) {
    audio.play();
  }
});

4.5 用户体验监控 (RUM)

在真实用户监控 (RUM) 中,performance.now() 是计算许多关键性能指标的基础,例如:

  • LCP (Largest Contentful Paint): 最大内容绘制时间,虽然浏览器直接提供,但其计算依赖于高精度时间戳。
  • FID (First Input Delay): 首次输入延迟,前面已讨论。
  • TTI (Time to Interactive): 页面达到可交互状态的时间。
  • 各种自定义指标:例如某个特定组件加载完成或渲染完成的时间。

通过结合 PerformanceObserver API,我们可以更优雅地收集这些高精度的时间数据。

五、performance.now() 的高级使用技巧与注意事项

5.1 与 requestAnimationFrame 结合

requestAnimationFrame (rAF) 回调函数会接收一个 DOMHighResTimeStamp 参数,这个参数表示当前帧开始渲染时的时间。这个 timestampperformance.now() 有着相同的基准点 (timeOrigin) 和精度,因此它们可以无缝结合使用。

重要提示:

  • rAF 提供的 timestamp 是浏览器在准备渲染下一帧时提供的精确时间。
  • 避免在 rAF 回调中频繁地调用 performance.now(),因为 rAF 已经提供了所需的高精度时间戳。直接使用 rAFtimestamp 参数通常是最佳实践。
let lastTimestamp = 0;
function animateWithRAF(timestamp) {
  if (lastTimestamp !== 0) {
    const frameDuration = timestamp - lastTimestamp;
    // console.log(`帧间隔: ${frameDuration.toFixed(3)} ms`);
  }
  lastTimestamp = timestamp;

  // 动画逻辑...
  requestAnimationFrame(animateWithRAF);
}
requestAnimationFrame(animateWithRAF);

5.2 精度限制与安全性

前面已经详细讨论了为了应对计时攻击,浏览器可能会对 performance.now() 的精度进行限制。

如何检查当前精度?

你可以通过连续多次调用 performance.now() 并观察其返回值来大致判断当前环境的精度:

function checkPerformanceNowPrecision() {
  const values = [];
  for (let i = 0; i < 100; i++) {
    values.push(performance.now());
  }

  let minDiff = Infinity;
  for (let i = 1; i < values.length; i++) {
    const diff = values[i] - values[i - 1];
    if (diff > 0 && diff < minDiff) { // 找到最小的正差值
      minDiff = diff;
    }
  }

  if (minDiff === Infinity) {
    console.log("无法测量到有效的时间差,可能精度极低或环境异常。");
  } else {
    console.log(`当前 performance.now() 的最小可分辨时间差 (近似精度): ${minDiff.toFixed(6)} 毫秒`);
    if (minDiff > 0.1) { // 0.1ms = 100微秒
      console.warn("当前 performance.now() 的精度可能受到限制 (例如,低于 100 微秒)。考虑启用跨域隔离 (COOP/COEP) 以获取更高精度。");
    } else {
      console.log("当前 performance.now() 精度良好。");
    }
  }
}

checkPerformanceNowPrecision();

何时需要高精度?

  • 动画、游戏物理模拟等需要平滑、精确步进的场景。
  • 对极短时间操作进行微基准测试。
  • 需要计算亚毫秒级用户交互延迟的RUM指标。

如果你的应用不需要达到微秒级的精度,那么即使在精度受限的环境下,performance.now() 依然比 Date.now() 提供了更好的单调性和相对时间基准。

5.3 性能开销

performance.now() 的调用开销通常非常低,因为它直接访问操作系统提供的底层计时器。在大多数现代硬件和浏览器上,单次调用可以在纳秒级别完成。

然而,在极度性能敏感的循环中,如果每一步都频繁调用 performance.now(),累积的开销也可能变得可观。例如,在每帧渲染数万个对象的循环中,如果每个对象都独立计时,可能会产生不必要的开销。在这种情况下,更好的做法是在循环开始前记录一次时间,循环结束后再记录一次,而不是在循环内部每次迭代都调用。

5.4 与 performance.timing 的关系

performance.timing 是一个较旧的 Performance API 接口,它提供了页面加载和导航事件的各个阶段的时间戳。这些时间戳都是基于Unix纪元、以整数毫秒表示的。

例如:

console.log(`页面重定向时间: ${performance.timing.redirectEnd - performance.timing.redirectStart} 毫秒`);
console.log(`DOM内容加载完成时间: ${performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart} 毫秒`);

主要区别:

  • performance.timing 使用整数毫秒,精度较低。
  • performance.timing 的所有时间戳都相对于Unix纪元,而不是 timeOrigin
  • performance.timing 不具备单调性保证,可能受系统时间调整影响。

由于 performance.timing 的局限性,现在更推荐使用 PerformanceObserver 配合 PerformanceEntry 接口来获取更现代、高精度且基于 timeOrigin 的性能数据。PerformanceEntry 接口(例如 PerformanceNavigationTimingPerformanceResourceTiming)中的 startTimeduration 属性都是 DOMHighResTimeStamp 类型,提供了与 performance.now() 相同的高精度。

5.5 Web Workers 中的使用

performance.now() 在 Web Workers 中同样可用。在 Worker 环境中,performance.now()timeOrigin 通常是 Worker 脚本开始执行的时间点。这意味着 Worker 线程有自己独立的性能时间线,这对于测量 Worker 内部任务的执行时间非常有用,而不会受到主线程活动的影响。

// worker.js
self.onmessage = (e) => {
  if (e.data === 'start') {
    const workerStartTime = performance.now();
    // 模拟 Worker 中的复杂计算
    let sum = 0;
    for (let i = 0; i < 50000000; i++) {
      sum += Math.random();
    }
    const workerEndTime = performance.now();
    const duration = workerEndTime - workerStartTime;
    self.postMessage(`Worker 计算完成,耗时: ${duration.toFixed(3)} 毫秒`);
  }
};

// main.js (主线程)
const myWorker = new Worker('worker.js');
myWorker.onmessage = (e) => {
  console.log(`主线程收到 Worker 消息: ${e.data}`);
};

const mainThreadStartTime = performance.now();
myWorker.postMessage('start');
const mainThreadEndTime = performance.now();
console.log(`主线程发送 Worker 消息耗时: ${(mainThreadEndTime - mainThreadStartTime).toFixed(3)} 毫秒`);

// 模拟主线程繁忙,观察是否影响 Worker 计时
for (let i = 0; i < 10000000; i++) {
  Math.cos(i);
}
console.log(`主线程繁忙任务完成: ${performance.now().toFixed(3)} 毫秒`);

在这个例子中,即使主线程非常繁忙,Worker 内部的 performance.now() 计时也不会受到影响,因为它运行在独立的线程上,有自己的执行上下文和时间线。

六、DOMHighResTimeStamp 接口与相关概念

performance.now() 返回的值类型是 DOMHighResTimeStamp。这是一个在Web标准中定义的特殊数据类型,用于表示高精度的时间值。

6.1 什么是 DOMHighResTimeStamp

DOMHighResTimeStamp 是一个 double 类型的浮点数,它表示从 timeOrigin 到当前时间点所经过的毫秒数。它的特点是:

  • 高精度: 可以表示亚毫秒级的时间,通常至少精确到微秒。
  • 单调递增: 保证时间流逝方向正确,不会因系统时钟调整而倒退。
  • 相对时间: 相对于 timeOrigin,而不是绝对的Unix纪元。

6.2 其他使用 DOMHighResTimeStamp 的 API

除了 performance.now() 之外,Web平台还有许多其他API也使用 DOMHighResTimeStamp 来提供高精度的时间信息,这增强了整个性能测量生态系统的一致性:

  1. requestAnimationFrame 回调参数: 如前所述,requestAnimationFrame 的回调函数接收的第一个参数就是 DOMHighResTimeStamp,表示浏览器准备更新动画帧的时间点。
  2. PerformanceEntry 接口:
    这是 Performance API 的核心接口之一,所有性能事件(如资源加载、导航、用户标记等)都通过实现此接口的子接口来暴露数据。PerformanceEntry 具有以下关键属性:

    • name: 性能条目的名称。
    • entryType: 性能条目的类型(例如 ‘resource’, ‘navigation’, ‘mark’, ‘measure’)。
    • startTime: DOMHighResTimeStamp,表示性能条目开始的时间。
    • duration: DOMHighResTimeStamp,表示性能条目持续的时间。

    子接口示例:

    • PerformanceResourceTiming:用于测量单个资源的加载时间。
      // 通过 PerformanceObserver 监听资源加载
      new PerformanceObserver((entryList) => {
        for (const entry of entryList.getEntries()) {
          if (entry.entryType === 'resource') {
            console.log(`资源: ${entry.name}, 开始时间: ${entry.startTime.toFixed(3)}ms, 持续时间: ${entry.duration.toFixed(3)}ms`);
            // 其他详细时间如 entry.fetchStart, entry.responseEnd 等也都是 DOMHighResTimeStamp
          }
        }
      }).observe({ type: ['resource'] });
    • PerformanceNavigationTiming:用于测量页面导航的各个阶段时间。
      // 页面加载完成后可获取
      const navEntry = performance.getEntriesByType('navigation')[0];
      if (navEntry) {
        console.log(`DOM Content Loaded Time: ${(navEntry.domContentLoadedEventEnd - navEntry.domContentLoadedEventStart).toFixed(3)}ms`);
      }
    • PerformanceMarkPerformanceMeasure:用于在代码中设置自定义的性能标记和测量。

      performance.mark('myCustomStart');
      // 执行一些代码
      performance.mark('myCustomEnd');
      performance.measure('myCustomOperation', 'myCustomStart', 'myCustomEnd');
      
      const measures = performance.getEntriesByName('myCustomOperation');
      if (measures.length > 0) {
        console.log(`自定义操作 'myCustomOperation' 耗时: ${measures[0].duration.toFixed(3)} ms`);
      }
  3. PerformanceObserver 这是一个用于异步收集性能条目的API。它允许你注册一个回调函数,当浏览器记录到新的性能条目时,该回调函数会被调用,并提供一个包含 PerformanceEntry 对象的列表。这是现代性能监控的最佳实践。

这些API协同工作,共同构建了一个强大且统一的Web性能测量系统,而 DOMHighResTimeStamp 则是贯穿其中的核心时间数据类型。

七、最佳实践与未来展望

7.1 何时使用 performance.now()

  • 需要亚毫秒级精度时: 当毫秒级误差无法接受,例如精确测量短函数执行时间、动画帧间隔、用户输入延迟等。
  • 测量短期、瞬时操作时: 针对页面加载后发生的、生命周期较短的事件或代码块。
  • 动画和游戏循环: 确保动画流畅性、物理模拟稳定性。
  • 避免系统时间影响时: 需要一个单调递增、不受用户或NTP影响的时间源。

7.2 何时使用 Date.now()

  • 记录绝对时间点: 例如记录日志、存储事件发生的时间到数据库、显示用户可见的当前时间。
  • 不需要高精度时: 大多数业务逻辑中的时间戳需求,毫秒级精度已经足够。
  • 与服务器时间同步: 如果需要与服务器的绝对时间进行比较或同步,Date.now() 更合适(但仍需考虑时区和时钟漂移)。

7.3 如何处理精度限制?

  • 测试生产环境: 在你的目标用户环境中测试 performance.now() 的实际精度。
  • 启用跨域隔离: 如果你的应用确实需要微秒级精度,并且可以接受跨域隔离带来的限制(例如,对第三方脚本和资源的加载有更严格的要求),那么配置 COOPCOEP 是获取全精度的途径。
  • 考虑备用方案或降级处理: 如果无法获得所需精度,评估影响。对于某些场景,降低精度可能不影响核心功能。例如,一个动画可能在100微秒精度下仍然足够流畅。

7.4 未来发展

Web平台对性能优化的投入是持续的。随着 WebAssembly、SharedArrayBuffer 等技术的普及,开发者将有能力在Web上构建更复杂、性能要求更高的应用。高精度计时器作为这些技术的基础设施,其重要性将只增不减。

未来可能会看到:

  • 更细粒度的精度控制API: 开发者或许能够更明确地请求或查询不同粒度的计时器精度。
  • 更强大的性能工具: 浏览器开发者工具将继续集成和可视化这些高精度数据,帮助开发者更直观地发现性能瓶颈。
  • 与其他Web API的深度融合: performance.now()DOMHighResTimeStamp 将在更多Web API中发挥作用,统一时间测量标准。

八、高精度时间戳:Web性能优化的基石

performance.now() 提供的高精度、单调递增且独立于系统时钟的时间戳,是现代Web性能优化的基石。它使得开发者能够以前所未有的精确度测量代码执行、同步动画、分析用户交互,并最终打造出更加流畅、响应迅速的用户体验。理解其工作原理、应用场景以及潜在的安全考量,是每一位追求卓越性能的Web开发者不可或缺的技能。

谢谢大家。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注