开发者社区> 行者武松> 正文

测试JavaScript函数的性能

简介:
+关注继续查看

测试JavaScript函数的性能


在软件中,性能一直扮演着重要的角色。在Web应用中,性能变得更加重要,因为如果页面速度很慢的话,用户就会很容易转去访问我们的竞争对手的网站。作为专业的web开发人员,我们必须要考虑这个问题。有很多“古老”的关于性能优化的最佳实践在今天依然可行,例如最小化请求数目,使用CDN以及不编写阻塞页面渲染的代码。然而,随着越来越多的web应用都在使用JavaScript,确保我们的代码运行的很快就变得很重要。

假设你有一个正在工作的函数,但是你怀疑它运行得没有期望的那样快,并且你有一个改善它性能的计划。那怎么去证明这个假设呢?在今天,有什么最佳实践可以用来测试JavaScript函数的性能呢?一般来说,完成这个任务的最佳方式是使用内置的performance.now()函数,来衡量函数运行前和运行后的时间。

在这篇文章中,我们会讨论如何衡量代码运行时间,以及有哪些技术可以避免一些常见的“陷阱”。

Performance.now()

高分辨率时间API提供了一个名为now()的函数,它返回一个DOMHighResTimeStamp对象,这是一个浮点数值,以毫秒级别(精确到千分之一毫秒)显示当前时间。单独这个数值并不会为你的分析带来多少价值,但是两个这样的数值的差值,就可以精确描述过去了多少时间。

这个函数除了比内置的Date对象更加精确以外,它还是“单调”的,简单说,这意味着它不会受操作系统(例如,你笔记本上的操作系统)周期性修改系统时间影响。更简单的说,定义两个Date实例,计算它们的差值,并不代表过去了多少时间。

“单调性”的数学定义是“(一个函数或者数值)以从不减少或者从不增加的方式改变”。

我们可以从另外一种途径来解释它,即想象使用它来在一年中让时钟向前或者向后改变。例如,当你所在国家的时钟都同意略过一个小时,以便最大化利用白天的时间。如果你在时钟修改之前创建了一个Date实例,然后在修改之后创建了另外一个,那么查看这两个实例的差值,看上去可能像“1小时零3秒又123毫秒”。而使用两个performance.now()实例,差值会是“3秒又123毫秒456789之一毫秒”。

在这一节中,我不会涉及这个API的过多细节。如果你想学习更多相关知识或查看更多如何使用它的示例,我建议你阅读这篇文章:Discovering the High Resolution Time API。

既然你知道高分辨率时间API是什么以及如何使用它,那么让我们继续深入看一下它有哪些潜在的缺点。但是在此之前,我们定义一个名为makeHash()的函数,在这篇文章剩余的部分,我们会使用它。


  1. function makeHash(source) { 
  2.  
  3.  var hash = 0; 
  4.  
  5.  if (source.length === 0) return hash; 
  6.  
  7.  for (var i = 0; i < source.length; i++) { 
  8.  
  9.    var char = source.charCodeAt(i); 
  10.  
  11.    hash = ((hash<<5)-hash)+char
  12.  
  13.    hash = hash & hash; // Convert to 32bit integer 
  14.  
  15.  } 
  16.  
  17.  return hash; 
  18.  

我们可以通过下面的代码来衡量这个函数的执行效率:


  1. var t0 = performance.now(); 
  2.  
  3. var result = makeHash('Peter'); 
  4.  
  5. var t1 = performance.now(); 
  6.  
  7. console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result); 

如果你在浏览器中运行这些代码,你应该看到类似下面的输出:


  1. Took 0.2730 milliseconds to generate: 77005292 

这段代码的在线演示如下所示:

记住这个示例后,让我们开始下面的讨论。

缺陷1 – 意外衡量不重要的事情

在上面的示例中,你可以注意到,我们在两次调用performance.now()中间只调用了makeHash()函数,然后将它的值赋给result变量。这给我们提供了函数的执行时间,而没有其他的干扰。我们也可以按照下面的方式来衡量代码的效率:


  1. var t0 = performance.now(); 
  2.  
  3. console.log(makeHash('Peter')); // bad idea! 
  4.  
  5. var t1 = performance.now(); 
  6.  
  7. console.log('Took', (t1 - t0).toFixed(4), 'milliseconds'); 

这个代码片段的在线演示如下所示:

但是在这种情况下,我们将会测量调用makeHash(‘Peter’)函数花费的时间,以及将结果发送并打印到控制台上花费的时间。我们不知道这两个操作中每个操作具体花费多少时间, 只知道总的时间。而且,发送和打印输出的操作所花费的时间会依赖于所用的浏览器,甚至依赖于当时的上下文。

或许你已经完美的意识到console.log方式是不可以预测的。但是执行多个函数同样是错误的,即使每个函数都不会触发I/O操作。例如:


  1. var t0 = performance.now(); 
  2.  
  3. var name = 'Peter'
  4.  
  5. var result = makeHash(name.toLowerCase()).toString(); 
  6.  
  7. var t1 = performance.now(); 
  8.  
  9. console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result); 

同样,我们不会知道执行时间是怎么分布的。它会是赋值操作、调用toLowerCase()函数或者toString()函数吗?

缺陷 #2 – 只衡量一次

另外一个常见的错误是只衡量一次,然后汇总花费的时间,并以此得出结论。很可能执行不同的次数会得出完全不同的结果。执行时间依赖于很多因素:

  • 编辑器热身的时间(例如,将代码编译成字节码的时间)
  • 主线程可能正忙于其它一些我们没有意识到的事情
  • 你的电脑的CPU可能正忙于一些会拖慢浏览器速度的事情

持续改进的方法是重复执行函数,就像这样:


  1. var t0 = performance.now(); 
  2.  
  3. for (var i = 0; i < 10; i++) { 
  4.  
  5.  makeHash('Peter'); 
  6.  
  7.  
  8. var t1 = performance.now(); 
  9.  
  10. console.log('Took', ((t1 - t0) / 10).toFixed(4), 'milliseconds to generate'); 

这个示例的在线演示如下所示:

这种方法的风险在于我们的浏览器的JavaScript引擎可能会使用一些优化措施,这意味着当我们第二次调用函数时,如果输入时相同的,那么JavaScript引擎可能会记住了第一次调用的输出,然后简单的返回这个输出。为了解决这个问题,你可以使用很多不同的输入字符串,而不用重复的使用相同的输入(例如‘Peter’)。显然,使用不同的输入进行测试带来的问题就是我们衡量的函数会花费不同的时间。或许其中一些输入会花费比其它输入更长的执行时间。

缺陷 #3 – 太依赖平均值

在上一节中,我们学习到的一个很好的实践是重复执行一些操作,理想情况下使用不同的输入。然而,我们要记住使用不同的输入带来的问题,即某些输入的执行时间可能会花费所有其它输入的执行时间都长。这样让我们退一步来使用相同的输入。假设我们发送同样的输入十次,每次都打印花费了多长时间。我们会得到像这样的输出:


  1. Took 0.2730 milliseconds to generate: 77005292 
  2.  
  3. Took 0.0234 milliseconds to generate: 77005292 
  4.  
  5. Took 0.0200 milliseconds to generate: 77005292 
  6.  
  7. Took 0.0281 milliseconds to generate: 77005292 
  8.  
  9. Took 0.0162 milliseconds to generate: 77005292 
  10.  
  11. Took 0.0245 milliseconds to generate: 77005292 
  12.  
  13. Took 0.0677 milliseconds to generate: 77005292 
  14.  
  15. Took 0.0289 milliseconds to generate: 77005292 
  16.  
  17. Took 0.0240 milliseconds to generate: 77005292 
  18.  
  19. Took 0.0311 milliseconds to generate: 77005292 

请注意第一次时间和其它九次的时间完全不一样。这很可能是因为浏览器中的JavaScript引擎使用了优化措施,需要一些热身时间。我们基本上没有办法避免这种情况,但是会有一些好的补救措施来阻止我们得出一些错误的结论。

一种方式是去计算后面9次的平均时间。另外一种更加使用的方式是收集所有的结果,然后计算“中位数”。基本上,它会将所有的结果排列起来,对结果进行排序,然后取中间的一个值。这是performance.now()函数如此有用的地方,因为无论你做什么,你都可以得到一个数值。

让我们再试一次,这次我们使用中位数函数:


  1. var numbers = []; 
  2.  
  3. for (var i=0; i < 10; i++) { 
  4.  
  5.  var t0 = performance.now(); 
  6.  
  7.  makeHash('Peter'); 
  8.  
  9.  var t1 = performance.now(); 
  10.  
  11.  numbers.push(t1 - t0); 
  12.  
  13.  
  14.   
  15.  
  16. function median(sequence) { 
  17.  
  18.  sequence.sort();  // note that direction doesn't matter 
  19.  
  20.  return sequence[Math.ceil(sequence.length / 2)]; 
  21.  
  22.  
  23.   
  24.  
  25. console.log('Median time', median(numbers).toFixed(4), 'milliseconds'); 

缺陷 #4 – 以可预测的方式比较函数

我们已经理解衡量一些函数很多次并取平均值总会是一个好主意。而且,上面的示例告诉我们使用中位数要比平均值更好。

在实际中,衡量函数执行时间的一个很好的用处是来了解在几个函数中,哪个更快。假设我们有两个函数,它们的输入参数类型一致,输出结果相同,但是它们的内部实现机制不一样。

例如,我们希望有一个函数,当特定的字符串在一个字符串数组中存在时,函数返回true或者false,但这个函数在比较字符串时不关心大小写。换句话说,我们不能直接使用Array.prototype.indexOf方法,因为这个方法是大小写敏感的。下面是这个函数的一个实现:


  1. function isIn(haystack, needle) { 
  2.  
  3.  var found = false
  4.  
  5.  haystack.forEach(function(element) { 
  6.  
  7.    if (element.toLowerCase() === needle.toLowerCase()) { 
  8.  
  9.      found = true
  10.  
  11.    } 
  12.  
  13.  }); 
  14.  
  15.  return found; 
  16.  
  17.  
  18.   
  19.  
  20. console.log(isIn(['a','b','c'], 'B'));  // true 
  21.  
  22. console.log(isIn(['a','b','c'], 'd'));  // false 

我们可以立刻发现这个方法有改进的地方,因为haystack.forEach循环总会遍历所有的元素,即使我们可以很快找到一个匹配的元素。现在让我们使用for循环来编写一个更好的版本。


  1. function isIn(haystack, needle) { 
  2.  
  3.  for (var i = 0, len = haystack.length; i < len; i++) { 
  4.  
  5.    if (haystack[i].toLowerCase() === needle.toLowerCase()) { 
  6.  
  7.      return true
  8.  
  9.    } 
  10.  
  11.  } 
  12.  
  13.  return false
  14.  
  15.  
  16.   
  17.  
  18. console.log(isIn(['a','b','c'], 'B'));  // true 
  19.  
  20. console.log(isIn(['a','b','c'], 'd'));  // false 

现在我们来看哪个函数更快一些。我们可以分别运行每个函数10次,然后收集所有的测量结果:


  1. function isIn1(haystack, needle) { 
  2.  
  3.  var found = false
  4.  
  5.  haystack.forEach(function(element) { 
  6.  
  7.    if (element.toLowerCase() === needle.toLowerCase()) { 
  8.  
  9.      found = true
  10.  
  11.    } 
  12.  
  13.  }); 
  14.  
  15.  return found; 
  16.  
  17.  
  18.   
  19.  
  20. function isIn2(haystack, needle) { 
  21.  
  22.  for (var i = 0, len = haystack.length; i < len; i++) { 
  23.  
  24.    if (haystack[i].toLowerCase() === needle.toLowerCase()) { 
  25.  
  26.      return true
  27.  
  28.    } 
  29.  
  30.  } 
  31.  
  32.  return false
  33.  
  34.  
  35.   
  36.  
  37. console.log(isIn1(['a','b','c'], 'B'));  // true 
  38.  
  39. console.log(isIn1(['a','b','c'], 'd'));  // false 
  40.  
  41. console.log(isIn2(['a','b','c'], 'B'));  // true 
  42.  
  43. console.log(isIn2(['a','b','c'], 'd'));  // false 
  44.  
  45.   
  46.  
  47. function median(sequence) { 
  48.  
  49.  sequence.sort();  // note that direction doesn't matter 
  50.  
  51.  return sequence[Math.ceil(sequence.length / 2)]; 
  52.  
  53.  
  54.   
  55.  
  56. function measureFunction(func) { 
  57.  
  58.  var letters = 'a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z'.split(','); 
  59.  
  60.  var numbers = []; 
  61.  
  62.  for (var i = 0; i < letters.length; i++) { 
  63.  
  64.    var t0 = performance.now(); 
  65.  
  66.    func(letters, letters[i]); 
  67.  
  68.    var t1 = performance.now(); 
  69.  
  70.    numbers.push(t1 - t0); 
  71.  
  72.  } 
  73.  
  74.  console.log(func.name'took', median(numbers).toFixed(4)); 
  75.  
  76.  
  77.   
  78.  
  79. measureFunction(isIn1); 
  80.  
  81. measureFunction(isIn2); 

我们运行上面的代码, 可以得出如下的输出:


  1. true 
  2.  
  3. false 
  4.  
  5. true 
  6.  
  7. false 
  8.  
  9. isIn1 took 0.0050 
  10.  
  11. isIn2 took 0.0150 

这个示例的在线演示如下所示:

到底发生了什么?第一个函数的速度要快3倍!那不是我们假设的情况。

其实假设很简单,但是有些微妙。第一个函数使用了haystack.forEach方法,浏览器的JavaScript引擎会为它提供一些底层的优化,但是当我们使用数据索引技术时,JavaScript引擎没有提供对应的优化。这告诉我们:在真正测试之前,你永远不会知道。

结论

在我们试图解释如何使用performance.now()方法得到JavaScript精确执行时间的过程中,我们偶然发现了一个基准场景,它的运行结果和我们的直觉相反。问题在于,如果你想要编写更快的web应用,我们需要优化JavaScript代码。因为计算机(几乎)是一个活生生的东西,它很难预测,有时会带来“惊喜”,所以如果了解我们代码是否运行更快,最可靠的方式就是编写测试代码并进行比较。

当我们有多种方式来做一件事情时,我们不知道哪种方式运行更快的另一个原因是要考虑上下文。在上一节中,我们执行一个大小写不敏感的字符串查询来寻找1个字符串是否在其它26个字符串中。当我们换一个角度来比较1个字符串是否在其他100,000个字符串中时,结论可能是完全不同的。

上面的列表不是很完整的,因为还有更多的缺陷需要我们去发现。例如,测试不现实的场景或者只在JavaScript引擎上测试。但是确定的是对于JavaScript开发者来说,如果你想编写更好更快的Web应用,performance.now()是一个很棒的方法。最后但并非最不重要,请谨记衡量执行时间只是“更好的代码”的一反面。我们还要考虑内存消耗以及代码复杂度。

怎么样?你是否曾经使用这个函数来测试你的代码性能?如果没有,那你是怎么来测试性能的?请在下面的评论中分享你的想法,让我们开始讨论吧!


作者:伯乐在线

来源:51CTO

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
全链路跟踪(压测)必备基础组件之线程上下文“三剑客”
全链路跟踪(压测)必备基础组件之线程上下文“三剑客”
9 0
Pandas用到今天,没成想竟忽略了这个函数
Pandas素以API丰富著称,个人也是常常沉醉于其中的各种骚操作而不能自拔(好吧,有些言重了)。近日,发现了一个前期一直忽略了的函数,仔细探索之下,发现竟然还有一些好用的功能,这个函数就是——transform。
3 0
IDEA+SSM+SpringBoot+Vue+Element UI实现班级管理增删改查
IDEA+SSM+SpringBoot+Vue+Element UI实现班级管理增删改查
2 0
微服务新秀之Nacos,看了就会,我说的!
本文主要介绍 微服务中的Nacos
4 0
SQL、Pandas和Spark:如何实现数据透视表?
数据透视表是一个很重要的数据统计操作,最有代表性的当属在Excel中实现(甚至说提及Excel,个人认为其最有用的当属三类:好用的数学函数、便捷的图表制作以及强大的数据透视表功能)。所以,今天本文就围绕数据透视表,介绍一下其在SQL、Pandas和Spark中的基本操作与使用,这也是沿承这一系列的文章之一。
5 0
Pandas中实现聚合统计,有几种方法?
Pandas提供了功能强大且灵活多样的API,可以满足使用者在数据分析和处理中的多种选择和实现方式。今天本文以Pandas中实现分组计数这个最基础的聚合统计功能为例,分享多种实现方案,最后一种应该算是一个骚操作了……
3 0
基于SSM+SpringBoot+Vue+ElmentUI实现公司案件管理系统
基于SSM+SpringBoot+Vue+ElmentUI实现公司案件管理系统
3 0
阿里云ECS续领文章
阿里云首次使用小计
3 0
详解Pandas读取csv文件时2个有趣的参数设置
Pandas其提供了从数据读取、数据预处理到数据分析以及数据可视化的全流程操作。其中,在数据读取阶段,应用pd.read_csv读取csv文件是常用的文件存储格式之一。今天,本文就来分享关于pandas读取csv文件时2个非常有趣且有用的参数。
4 0
Pandas用了这么久,有觉得哪里不好的地方吗?
作为一名数据分析师,自己对Pandas有过系统的学习和应用实践,对其大部分功能甚至骚操作也称得上有所研究,前期也写过太多的Pandas应用技巧相关的文章。那么在赞美之余,有没有一些觉得不好的设计呢?今天本文就来吐槽3个自己觉得Pandas设计有欠妥当的地方,纯为个人见解!
5 0
+关注
行者武松
杀人者,打虎武松也。
17141
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载