各位听众,大家下午好。
今天,我们将深入探讨Web前端性能优化中一个至关重要的工具——Performance API 中的 performance.now() 方法。这个方法提供的高精度时间戳,对于我们精确测量代码执行时间、优化用户体验、调试复杂交互等方面,具有不可替代的价值。我们将从其基本概念出发,逐步揭示其背后的浏览器时间源机制,探讨它在各种场景下的实际应用,并讨论其高级特性、安全考量以及未来的发展。
一、引言:为什么我们需要高精度时间戳?
在Web开发的早期,我们获取时间戳的常见方式是使用 Date.now()。它返回自Unix纪元(1970年1月1日00:00:00 UTC)以来的毫秒数。对于大多数日常任务,例如记录日志、显示当前时间或者计算两个事件之间的大致间隔,Date.now() 已经足够了。
然而,随着Web应用日益复杂,用户对性能和流畅度的要求也越来越高。当我们需要测量微小的性能瓶颈、精确同步动画、或者分析复杂的用户交互延迟时,Date.now() 的局限性就暴露无遗了:
- 精度不足:
Date.now()的精度通常是毫秒级,这意味着它无法区分在同一毫秒内发生的多个事件。对于需要亚毫秒(微秒甚至纳秒)精度的场景,例如测量一个短函数执行时间、计算渲染帧率或者评估用户输入延迟(FID – First Input Delay),毫秒级的精度是远远不够的。 - 非单调性:
Date.now()是基于系统时钟的。如果用户手动更改了系统时间,或者系统通过NTP(网络时间协议)自动同步时间,Date.now()返回的值可能会“跳跃”,甚至倒退。这会使得我们无法可靠地计算时间间隔,因为endTime - startTime可能会得到一个负值或异常大的值。 - 基准点问题:
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.timeOrigin。timeOrigin 通常代表以下时间点之一:
- 对于主文档 (
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() 也不会受到影响,它将继续以稳定的速率递增。
这种单调性对于精确测量时间间隔至关重要。例如,如果你在 t1 和 t2 时刻分别调用 performance.now(),那么 t2 - t1 总是能可靠地表示这两个时刻之间经过的时间,而不会因为系统时间调整而出现负值或不合理的结果。
2.4 时间源
performance.now() 背后依赖的是浏览器内部的高精度时钟,这个时钟通常与操作系统的单调时钟功能紧密集成。它不会受到网络时间同步或用户手动修改系统时间的影响,保证了时间戳的稳定性和可靠性。
三、浏览器时间源:如何实现高精度?
performance.now() 之所以能提供高精度且单调递增的时间戳,得益于底层操作系统和硬件的支持,并由浏览器进行了精巧的封装。
3.1 操作系统层面的支持
现代操作系统都提供了高精度的单调时钟接口,这些接口是 performance.now() 实现的基础:
- Windows: Windows 系统提供了
QueryPerformanceCounter和QueryPerformanceFrequency函数。QueryPerformanceCounter返回一个高分辨率的计数器值,而QueryPerformanceFrequency返回该计数器每秒的频率。通过这两个函数,可以计算出非常精确的时间间隔。这个计数器是系统启动以来一直递增的,不受系统时间调整影响。 - Linux/macOS: 类Unix系统(如Linux和macOS)通常使用
clock_gettime函数,并结合CLOCK_MONOTONIC或CLOCK_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环境。
这种封装确保了:
- 平台无关性: 开发者无需关心底层操作系统的差异。
- 安全性: 浏览器可以控制时间戳的暴露精度,以缓解潜在的安全风险(详见下文)。
- 标准化: 遵循Web标准,提供统一的API接口。
3.4 安全与隐私考虑
高精度计时器虽然强大,但也带来了一些安全和隐私风险,主要是 计时攻击 (Timing Attacks)。
计时攻击: 恶意网站可以通过精确测量JavaScript代码的执行时间,来推断出一些敏感信息,例如:
- 用户是否登录了某个网站: 测量访问受保护资源所需的时间,如果用户已登录,响应时间可能更快。
- CPU缓存状态: 测量访问不同内存地址所需的时间,推断CPU缓存中的数据,进而可能泄露其他进程或同源网站的敏感信息。
- 侧信道攻击 (Side-channel attacks) 的基础: 例如,在 Spectre 和 Meltdown 等CPU漏洞被发现后,高精度计时器被认为是进行侧信道攻击的关键工具之一。攻击者可以通过测量内存访问时间来推断出被隔离的敏感数据。
为了缓解这些风险,浏览器厂商采取了一些策略来限制 performance.now() 的精度:
- 精度限制 (Precision Throttling):
- 在某些情况下(例如,页面在后台运行、跨域Iframe中),浏览器可能会动态降低
performance.now()的精度,将其舍入到最近的整数毫秒或更高的值(例如,100微秒)。 - Chrome、Firefox 等浏览器在没有启用
SharedArrayBuffer或WebAssembly.Memory的情况下,会将performance.now()的精度限制在 100 微秒 (0.1 毫秒) 左右。 - 这意味着,即使底层硬件和操作系统能够提供纳秒级精度,JavaScript 代码也可能只能获得较低精度的结果。
- 在某些情况下(例如,页面在后台运行、跨域Iframe中),浏览器可能会动态降低
- 跨域隔离 (Cross-origin Isolation):
为了重新启用SharedArrayBuffer和高精度计时器(包括performance.now()的全精度),网站需要启用 跨域隔离。这通常通过设置两个HTTP响应头来实现:Cross-Origin-Opener-Policy: same-origin(COOP)Cross-Origin-Embedder-Policy: require-corp(COEP)
当页面处于跨域隔离状态时,它会确保自身不会加载任何未经COEP授权的跨域资源,并且不会与非跨域隔离的文档共享浏览上下文。这种严格的隔离环境减少了计时攻击的威胁,因此浏览器可以放心地提供高精度计时器。
总结一下: 如果你的网站需要 performance.now() 的最高精度(微秒级),你可能需要考虑配置 COOP 和 COEP 来启用跨域隔离。否则,在默认情况下,你获得的精度可能会受到限制。
四、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 参数,这个参数表示当前帧开始渲染时的时间。这个 timestamp 与 performance.now() 有着相同的基准点 (timeOrigin) 和精度,因此它们可以无缝结合使用。
重要提示:
rAF提供的timestamp是浏览器在准备渲染下一帧时提供的精确时间。- 避免在
rAF回调中频繁地调用performance.now(),因为rAF已经提供了所需的高精度时间戳。直接使用rAF的timestamp参数通常是最佳实践。
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 接口(例如 PerformanceNavigationTiming 或 PerformanceResourceTiming)中的 startTime 和 duration 属性都是 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 来提供高精度的时间信息,这增强了整个性能测量生态系统的一致性:
requestAnimationFrame回调参数: 如前所述,requestAnimationFrame的回调函数接收的第一个参数就是DOMHighResTimeStamp,表示浏览器准备更新动画帧的时间点。-
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`); }-
PerformanceMark和PerformanceMeasure:用于在代码中设置自定义的性能标记和测量。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`); }
PerformanceObserver: 这是一个用于异步收集性能条目的API。它允许你注册一个回调函数,当浏览器记录到新的性能条目时,该回调函数会被调用,并提供一个包含PerformanceEntry对象的列表。这是现代性能监控的最佳实践。
这些API协同工作,共同构建了一个强大且统一的Web性能测量系统,而 DOMHighResTimeStamp 则是贯穿其中的核心时间数据类型。
七、最佳实践与未来展望
7.1 何时使用 performance.now()?
- 需要亚毫秒级精度时: 当毫秒级误差无法接受,例如精确测量短函数执行时间、动画帧间隔、用户输入延迟等。
- 测量短期、瞬时操作时: 针对页面加载后发生的、生命周期较短的事件或代码块。
- 动画和游戏循环: 确保动画流畅性、物理模拟稳定性。
- 避免系统时间影响时: 需要一个单调递增、不受用户或NTP影响的时间源。
7.2 何时使用 Date.now()?
- 记录绝对时间点: 例如记录日志、存储事件发生的时间到数据库、显示用户可见的当前时间。
- 不需要高精度时: 大多数业务逻辑中的时间戳需求,毫秒级精度已经足够。
- 与服务器时间同步: 如果需要与服务器的绝对时间进行比较或同步,
Date.now()更合适(但仍需考虑时区和时钟漂移)。
7.3 如何处理精度限制?
- 测试生产环境: 在你的目标用户环境中测试
performance.now()的实际精度。 - 启用跨域隔离: 如果你的应用确实需要微秒级精度,并且可以接受跨域隔离带来的限制(例如,对第三方脚本和资源的加载有更严格的要求),那么配置
COOP和COEP是获取全精度的途径。 - 考虑备用方案或降级处理: 如果无法获得所需精度,评估影响。对于某些场景,降低精度可能不影响核心功能。例如,一个动画可能在100微秒精度下仍然足够流畅。
7.4 未来发展
Web平台对性能优化的投入是持续的。随着 WebAssembly、SharedArrayBuffer 等技术的普及,开发者将有能力在Web上构建更复杂、性能要求更高的应用。高精度计时器作为这些技术的基础设施,其重要性将只增不减。
未来可能会看到:
- 更细粒度的精度控制API: 开发者或许能够更明确地请求或查询不同粒度的计时器精度。
- 更强大的性能工具: 浏览器开发者工具将继续集成和可视化这些高精度数据,帮助开发者更直观地发现性能瓶颈。
- 与其他Web API的深度融合:
performance.now()和DOMHighResTimeStamp将在更多Web API中发挥作用,统一时间测量标准。
八、高精度时间戳:Web性能优化的基石
performance.now() 提供的高精度、单调递增且独立于系统时钟的时间戳,是现代Web性能优化的基石。它使得开发者能够以前所未有的精确度测量代码执行、同步动画、分析用户交互,并最终打造出更加流畅、响应迅速的用户体验。理解其工作原理、应用场景以及潜在的安全考量,是每一位追求卓越性能的Web开发者不可或缺的技能。
谢谢大家。