Web应用性能始终至关重要,尤其在网页开发中,缓慢的页面加载速度会直接导致用户流失。作为专业的前端开发者,我们必须重视性能优化。许多传统的网页性能优化方法,例如减少请求次数、使用CDN以及避免编写阻塞渲染的代码,至今仍然有效。然而,随着越来越多的Web应用使用JavaScript,验证代码效率变得至关重要。
假设您有一个功能正常的函数,但怀疑其效率不高,并计划进行改进。如何证明这一假设?目前测试JavaScript函数性能的最佳实践是什么?通常,最佳方法是使用内置的performance.now()
函数,测量函数执行前后的时间。本文将讨论如何测量代码执行时间以及避免一些常见陷阱的技巧。
关键要点
performance.now()
函数来测量函数执行前后的时间。now()
的函数,它返回一个DOMHighResTimeStamp
对象。这提供了对已过去时间的精确描述,使其比内置的Date
对象更精确。performance.now()
之间唯一测量的操作是所讨论函数的执行。performance.now()
高分辨率时间API提供了一个名为now()
的函数,它返回一个DOMHighResTimeStamp
对象。这是一个浮点数,以毫秒为单位反映当前时间,精确到千分之一毫秒
。单独来看,这个数字对您的分析并没有多大价值,但是两个这样的数字之间的差值可以精确地描述已经过去了多少时间。除了比内置的Date
对象更精确之外,它也是“单调的”。这意味着,简单来说,它不受系统(例如您的笔记本电脑操作系统)定期校正系统时间的影响。更简单地说,定义Date
的两个实例并计算差值并不能代表已过去的时间。“单调”的数学定义是(对于函数或数量)以这样一种方式变化,即它要么从不减少,要么从不增加
。另一种解释方法是,试想一下在一年中时钟向前或向后拨动的时间段内使用它。例如,当您所在国家的时钟都同意为了最大限度地利用白天阳光而跳过一小时时。如果您要在时钟拨回一小时之前创建一个Date
实例,然后在之后再创建一个Date
实例,查看差异,它会显示类似“1小时3秒123毫秒”的内容。使用performance.now()
的两个实例,差异将是“3秒123毫秒和456789千分之一毫秒”。在本节中,我不会详细介绍此API。因此,如果您想了解更多信息并查看其使用的一些示例,我建议您阅读文章《Discovering the High Resolution Time API》。既然您已经了解了高分辨率时间API是什么以及如何使用它,让我们深入探讨一些潜在的陷阱。但在这样做之前,让我们定义一个名为makeHash()
的函数,我们将在本文的其余部分使用它。
<code class="language-javascript">function makeHash(source) { var hash = 0; if (source.length === 0) return hash; for (var i = 0; i < source.length; i++) { var char = source.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; }</code>
此函数的执行可以按如下所示进行测量:
<code class="language-javascript">var t0 = performance.now(); var result = makeHash('Peter'); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);</code>
如果您在浏览器中运行此代码,您应该会看到类似以下内容:
<code>Took 0.2730 milliseconds to generate: 77005292</code>
下面显示了此代码的实时演示:
[此处应插入CodePen链接,由于我无法访问外部网站,无法提供]
考虑到这个例子,让我们开始讨论。
在上面的示例中,您可以注意到,在一次performance.now()
和另一次performance.now()
之间,我们唯一做的就是调用函数makeHash()
并将它的值赋给变量result
。这给了我们执行该函数所需的时间,而没有其他任何内容。此测量也可以按如下所示进行:
<code class="language-javascript">var t0 = performance.now(); console.log(makeHash('Peter')); // 不好的主意! var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds');</code>
下面显示了此代码段的实时演示:
[此处应插入CodePen链接,由于我无法访问外部网站,无法提供]
但在这种情况下,我们将测量调用函数makeHash('Peter')
所需的时间以及在控制台上发送和打印该输出所需的时间。我们不知道这两个操作分别需要多长时间。您只知道组合时间。此外,发送和打印输出所需的时间会根据浏览器甚至当时浏览器中发生的情况而有很大差异。
也许您完全知道console.log
不可预测地慢。但是,执行多个函数也是同样错误的,即使每个函数都不涉及任何I/O。例如:
<code class="language-javascript">function makeHash(source) { var hash = 0; if (source.length === 0) return hash; for (var i = 0; i < source.length; i++) { var char = source.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; }</code>
同样,我们不知道执行时间是如何分配的。是变量赋值、toLowerCase()
调用还是toString()
调用?
另一个常见错误是只进行一次测量,总结所花费的时间,并根据该时间得出结论。在不同时间,这很可能完全不同。执行时间很大程度上取决于各种因素:
一种增量改进方法是重复执行该函数,如下所示:
<code class="language-javascript">var t0 = performance.now(); var result = makeHash('Peter'); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);</code>
下面显示了此示例的实时演示:
[此处应插入CodePen链接,由于我无法访问外部网站,无法提供]
这种方法的风险在于,我们的浏览器JavaScript引擎可能会进行次优化,这意味着第二次使用相同的输入调用该函数时,它可以从记住第一个输出中获益,并简单地再次使用它。为了解决这个问题,您可以使用许多不同的输入字符串,而不是重复发送相同的输入字符串(例如“Peter”)。显然,使用不同输入进行测试的问题在于,我们正在测量的函数自然会花费不同的时间。也许某些输入会导致比其他输入更长的执行时间。
在上一节中,我们了解到重复运行某些内容是一个好习惯,理想情况下应该使用不同的输入。但是,我们必须记住,不同输入的问题在于,执行时间可能比所有其他输入都长得多。因此,让我们退一步,发送相同的输入。假设我们发送相同的输入十次,并且每次都打印出花费的时间。输出可能如下所示:
<code class="language-javascript">function makeHash(source) { var hash = 0; if (source.length === 0) return hash; for (var i = 0; i < source.length; i++) { var char = source.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; }</code>
请注意,第一次,数字与其他九次完全不同。这很可能是因为我们浏览器中的JavaScript引擎进行了一些次优化,需要一些预热。我们几乎无法避免这种情况,但我们可以考虑一些好的补救措施来防止错误的结论。一种方法是计算最后九次的时间平均值。另一种更实用的方法是收集所有结果并计算中位数。基本上,这是按顺序排列的所有结果,并选择中间的一个。这就是performance.now()
如此有用的地方,因为您可以得到一个可以随意使用的数字。让我们再试一次,但这次我们将使用一个中位数函数:
<code class="language-javascript">var t0 = performance.now(); var result = makeHash('Peter'); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);</code>
我们已经了解到,多次测量某些内容并取平均值总是一个好主意。此外,最后一个例子告诉我们,最好使用中位数而不是平均值。现在,实际上,测量函数执行时间的一个很好的用途是了解几个函数中哪个更快。假设我们有两个函数,它们接受相同类型的输入并产生相同的结果,但在内部它们的工作方式不同。假设我们想要一个函数,如果某个字符串存在于其他字符串的数组中,则返回true或false,但这不区分大小写。换句话说,我们不能使用Array.prototype.indexOf
,因为它不区分大小写。这是一个这样的实现:
<code>Took 0.2730 milliseconds to generate: 77005292</code>
我们立即注意到,这可以改进,因为haystack.forEach
循环总是遍历所有元素,即使我们有一个早期匹配。让我们尝试使用一个好的旧for循环来编写一个更好的版本。
<code class="language-javascript">var t0 = performance.now(); console.log(makeHash('Peter')); // 不好的主意! var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds');</code>
现在让我们看看哪个更快。我们通过运行每个函数10次并收集所有测量结果来做到这一点:
<code class="language-javascript">var t0 = performance.now(); var name = 'Peter'; var result = makeHash(name.toLowerCase()).toString(); var t1 = performance.now(); console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);</code>
我们运行它并得到以下输出:
<code class="language-javascript">var t0 = performance.now(); for (var i = 0; i < 10; i++) { makeHash('Peter'); } var t1 = performance.now(); console.log('Took', ((t1 - t0) / 10).toFixed(4), 'milliseconds to generate');</code>
下面显示了此示例的实时演示:
[此处应插入CodePen链接,由于我无法访问外部网站,无法提供]
究竟发生了什么?第一个函数快了三倍。这本不应该发生!
解释很简单,但很微妙。使用haystack.forEach
的第一个函数受益于浏览器JavaScript引擎中的一些低级优化,而当我们使用数组索引技术时,我们不会得到这些优化。它证明了我们的观点:除非你测量它,否则你永远不会知道!
结论
在我们试图演示如何使用performance.now()
在JavaScript中获得精确的执行时间时,我们偶然发现了一个基准测试场景,在这个场景中,我们的直觉与我们的经验结果得出的结论恰恰相反。关键是,如果您想编写更快的Web应用程序,您的JavaScript代码需要进行优化。因为计算机(几乎)是活生生的东西,所以它们是不可预测和令人惊讶的。知道我们的代码改进会产生更快的执行速度的最可靠方法是测量和比较。我们永远不知道哪段代码更快,如果我们有多种方法来做同样的事情,另一个原因是上下文很重要。在上一节中,我们执行了一个不区分大小写的字符串搜索,在一个26个其他字符串中查找一个字符串。如果我们必须在100,000个其他字符串中查找一个字符串,那么结论很可能会完全不同。上面的列表并不详尽,因为还有更多需要注意的陷阱。例如,测量不切实际的场景或只在一个JavaScript引擎上进行测量。但可以肯定的是,对于想要编写更快、更好的Web应用程序的JavaScript开发者来说,performance.now()
是一个巨大的财富。最后但并非最不重要的一点是,请记住,测量执行时间只会产生“更好代码”的一个维度。还需要考虑内存和代码复杂性。您呢?您是否曾经使用此函数来测试代码的性能?如果没有,您在此阶段如何进行?请在下面的评论中分享您的想法。让我们开始讨论吧!
(FAQs部分略去,因为与上面内容高度重复,只需保留关键要点即可。)
以上是测量JavaScript功能&#x27;表现的详细内容。更多信息请关注PHP中文网其他相关文章!