衡量 JavaScript 函数的性能
这篇文章最初发表在我的博客上。
测量函数执行时间始终是证明某种实现方式比另一种更高效的好方法。它也是确保性能在更改后没有下降以及追踪性能瓶颈的有效途径。
良好的性能有助于提升用户体验。而良好的用户体验又能促使用户再次光顾。例如,这项研究表明,88% 的在线消费者在遭遇性能问题导致的糟糕用户体验后,不太可能再次访问该网站。
因此,能够识别代码中的瓶颈并衡量改进效果至关重要。尤其是在开发浏览器端的 JavaScript 时,必须意识到,由于 JavaScript 是单线程语言,你编写的每一行代码都可能阻塞 DOM 操作。
在本文中,我将解释如何衡量函数的性能以及如何处理从中得到的结果。
我在这里提到的这些函数非常适合对 JavaScript 函数进行底层调试。如果您希望确保您的应用程序即使在添加更多功能后也能保持快速运行,请考虑实施性能预算。
Performance.now
性能 API通过其函数提供对DOMHighResTimeStampperformance.now()的访问,该函数返回自页面加载以来经过的时间(以毫秒为单位),精度最高可达5µs(小数部分)。
所以实际上你需要获取两个时间戳,将它们保存在一个变量中,然后根据第一个时间戳计算第二个时间戳:
const t0 = performance.now();
for (let i = 0; i < array.length; i++)
{
// some code
}
const t1 = performance.now();
console.log(t1 - t0, 'milliseconds');
输出(Chrome):
0.6350000001020817 "milliseconds"
输出(Firefox):
1 milliseconds
我们可以看到,Firefox 的测试结果与 Chrome 截然不同。这是因为从 60 版本开始,Firefox 将性能 API 的精度降低到了 2 毫秒。您可以在本文末尾找到更多相关信息。
性能 API 的功能远不止返回时间戳。它还能测量导航时间、用户操作时间或资源运行时间。
请参阅这篇文章,了解更多详情。
但是,就我们的使用场景而言,我们只想测量单个函数的性能,因此时间戳就足够了。
那不就跟 Date.now 一样吗?
现在你可能会想,嘿,我也可以用Date.now它来做这件事。
可以,但这也有缺点。
Date.now返回自 Unix 纪元(“1970-01-01T00:00:00Z”)以来经过的时间,单位为毫秒,并且依赖于系统时钟。这不仅意味着它的精度不高,而且它也不是始终递增的。以下是 WebKit 工程师(Tony Gentilcore)的解释:
人们可能不太注意到,基于系统时间的日期数据也并非监控真实用户的理想选择。大多数系统都运行着一个守护进程,定期同步时间。通常情况下,系统时钟每 15-20 分钟就会进行几毫秒的微调。按照这种频率,大约 1% 的 10 秒间隔测量结果会存在误差。
控制台时间
这个 API 非常易用。只需在要测量的代码前后各console.time添加一个参数,并调用该函数即可。您可以在一个页面上同时使用多达 10,000 个计时器。console.timeEndstring
精度与性能 API 相同,但这同样取决于浏览器。
console.time('test');
for (let i = 0; i < array.length; i++) {
// some code
}
console.timeEnd('test');
这会自动生成如下所示的易于理解的输出:
输出(Chrome):
test: 0.766845703125ms
输出(Firefox):
test: 2ms - timer ended
此处的输出与性能 API 的输出非常相似。
console.time它的优点是使用起来更方便,因为它不需要手动计算两个时间戳之间的差值。
降低时间精度
如果您在不同的浏览器中使用上述 API 来衡量您的函数,您可能会注意到结果有所不同。
这是因为浏览器试图保护用户免受时间攻击和指纹识别的侵害,
如果时间戳过于精确,黑客可以利用这些攻击和指纹识别来识别用户。
例如,Firefox 等浏览器会尝试通过将精度降低到 2 毫秒(版本 60)来防止这种情况发生。
需要注意的事项
现在你已经拥有了衡量 JavaScript 函数运行速度所需的工具。但是,最好避免一些常见的陷阱:
分而治之
你注意到在筛选某些结果时速度很慢,但你不知道瓶颈在哪里。
与其胡乱猜测代码的哪一部分运行缓慢,不如使用上面提到的这些函数来测量它。
要找出问题所在,首先将你的console.time语句放在运行缓慢的代码块周围。然后测量这些语句中不同部分的性能。如果某个部分比其他部分慢,就继续测试该部分,并逐个深入,直到找到性能瓶颈。
这些语句之间的代码越少,就越不可能跟踪到你不感兴趣的东西。
注意输入值
在实际应用中,给定函数的输入值可能会发生很大变化。仅仅测量函数对任意随机值的执行速度,并不能为我们提供任何真正有价值的数据。
请务必使用相同的输入值运行代码。
多次运行该函数
假设你有一个函数,它遍历一个数组,对数组中的每个值进行一些计算,然后返回一个包含结果的数组。你想知道使用forEach简单的循环还是for使用函数性能更高。
这些是各项功能:
function testForEach(x) {
console.time('test-forEach');
const res = [];
x.forEach((value, index) => {
res.push(value / 1.2 * 0.1);
});
console.timeEnd('test-forEach')
return res;
}
function testFor(x) {
console.time('test-for');
const res = [];
for (let i = 0; i < x.length; i ++) {
res.push(x[i] / 1.2 * 0.1);
}
console.timeEnd('test-for')
return res;
}
测试方法如下:
const x = new Array(100000).fill(Math.random());
testForEach(x);
testFor(x);
如果在 Firefox 中运行上述函数,您将得到类似如下的输出:
test-forEach: 27ms - timer ended
test-for: 3ms - timer ended
看起来 forEach 的速度慢得多,对吧?
让我们看看如果使用相同的输入运行两次相同的函数会发生什么:
testForEach(x);
testForEach(x);
testFor(x);
testFor(x);
test-forEach: 13ms - timer ended
test-forEach: 2ms - timer ended
test-for: 1ms - timer ended
test-for: 3ms - timer ended
forEach如果再次调用该测试,其性能与循环相同。考虑到初始值较慢,for可能无论如何都不值得使用循环。forEach
……并且在多种浏览器中均可使用。
如果在 Chrome 浏览器中运行上述代码,结果就会截然不同:
test-forEach: 6.156005859375ms
test-forEach: 8.01416015625ms
test-for: 4.371337890625ms
test-for: 4.31298828125ms
这是因为Chrome和Firefox的JavaScript引擎不同,性能优化方式也不同。了解这些差异很有必要。
forEach在这种情况下,Firefox 在优化相同输入的使用方面做得更好。
for在两个引擎上的表现都更好,所以最好还是坚持使用for循环。
这正是为什么你应该在多个引擎中进行测试的好例子。如果你只在 Chrome 中进行测试,你可能会得出这样的结论:forEach与 . 相比,它的表现还算不错for。
限制 CPU 频率
这些数值看起来并不高。但请注意,您的开发机器通常比用户浏览您网站的普通手机快得多。
为了感受一下这种效果,浏览器提供了一项功能,可以限制 CPU 性能。
这样一来,原本的 10 毫秒或 50 毫秒很快就会变成 500 毫秒。
衡量相对表现
这些原始结果实际上不仅取决于您的硬件,还取决于您当前的 CPU 负载和 JavaScript 线程。请尽量关注测量结果的相对改进,因为下次重启计算机后,这些数字可能会有很大变化。
结论
在本文中,我们了解了一些可用于测量性能的 JavaScript API,以及如何在“实际应用”中使用它们。对于简单的测量,我发现使用起来更容易console.time。
我觉得很多前端开发人员日常工作中对性能的考虑不够,尽管性能直接影响收入。
如何确保在日常工作中不会忽略绩效考核?欢迎随时给我发邮件或推特,分享您的想法!
文章来源:https://dev.to/fgerschau/measuring-the-performance-of-javascript-functions-h6m