测试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

时间: 2024-10-22 06:23:19

测试JavaScript函数的性能的相关文章

javascript日期处理函数,性能优化批处理_基础知识

其实网上写javascript日期格式化的博文很多,大体都看了看,都还不错.唯一遗憾的是只顾着实现了功能,没对函数进行性能优化. 俗话说:不要重复造轮子.google上找了一个比较不错的日期格式化函数,来开始我的优化之旅吧! google上找的这个日期函数化函数,估计大家都很眼熟,以前我也一直在用.先看看优化后和优化前的效率对比吧! 1.优化之前的toDate函数(字符串转换成Date对象),重复执行1万次,耗时660毫秒 2.优化之前的dateFormat函数(Date对象格式化成字符串),重

javascript函数没有重载测试

  今天继续学习javascript系列教程,虽然是基础,但我觉得还是有必要用心来学习的,不要怕难,不用怕忘记,不要怕学不会.哪个高手不是从零开始的,我要坚定自己的学习信心,并且认真的走下去.虽然路途艰辛,但总会在尽头品尝到芬芳的味道. 函数是定义一次但可以调用或执行任意多次的一段js代码.函数有时会有参数,即函数被调用时指定了值的局部变量.函数常常使用这些参数来计算一个返回值,这个值也成为了函数调用表达式的值.  代码如下   function box(){ alert("年龄");

如何优化JavaScript脚本的性能

javascript|脚本|性能|优化 随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术.而现在Ajax则是最为流行的一种方式.JavaScript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能在客户端所做的事情,为了能改进他的性能,我想基于我以前给JavaScript做过的很多测试来谈谈自己的经验,希望能帮助大家改进自己的JavaScript脚本性能. 语言层次方面 循环 循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript

提高:如何优化JavaScript脚本的性能

javascript|脚本|性能|优化 随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术.而现在Ajax则是最为流行的一种方式.javascript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能在客户端所做的事情,为了能改进他的性能,我想基于我以前给JavaScript做过的很多测试来谈谈自己的经验,希望能帮助大家改进自己的JavaScript脚本性能. 语言层次方面 循环 循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript

网页教程:如何优化JavaScript脚本的性能

javascript|脚本|教程|网页|性能|优化 作者:ShiningRay @ Nirvana Studio 随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术.而现在Ajax则是最为流行的一种方式.javascript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能在客户端所做的事情,为了能改进他的性能,我想基于我以前给JavaScript做过的很多测试来谈谈自己的经验,希望能帮助大家改进自己的JavaScript脚本性能. 语言层次方面循环循环是

如何改进javascript代码的性能_javascript技巧

本来在那片编写可维护性代码文章后就要总结这篇代码性能文章的,耽搁了几天,本来也是决定每天都要更新一篇文章的,因为以前欠下太多东西没总结,学过的东西没去总结真的很快就忘记了,记录一下在你脑力留下更深的印象,特别是这些可维护性代码,性能什么的,当在你脑子里形成一种习惯了,那你就牛了!这里也要给初学者一个建议:多总结你学过的东西,因为这其实也是在学习新知识! 好,进入我们的主题:如何提高JS代码的性能. 1.优化DOM交互 DOM与我们的页面紧密相关,浏览器渲染页面也就是在渲染解析后的DOM元素,DO

JavaScript 函数

JavaScript 函数定义 JavaScript 使用关键字 function 定义函数 函数可以通过声明定义,也可以是一个表达式 函数声明 函数声明的语法 : function sum1(n1,n2){ return n1+n2; }; 函数声明后不会立即执行,会在我们需要的时候调用到 分号是用来分隔可执行JavaScript语句,由于函数声明不是一个可执行语句所以不以分号结束 函数表达式,又叫函数字面量 JavaScript 函数可以通过一个表达式定义 函数表达式可以存储在变量中: va

浅谈javascript函数中的this

1) 当一个函数被保存为对象的一个属性时,我们称它为一个方法.当一个方法被调用时,this被绑定到该对象. 2) 当一个函数并非一个对象的属性,那么当它被调用时,this被绑定到全局对象.这一缺陷导致内部函数的this不一定是调用它的函数的this,解决办法是在外部函数定义一个新的变量指向this(例如var that = this; ). 3) 在一个函数前面带上new,将创建一个新对象,this绑定到新对象. 4) apply可以模拟一个对象拥有并执行该函数的场景,第一个参数传递对象给thi

WEB开发者必备的7个JavaScript函数

防止高频调用的debounce函数 这个 debounce 函数对于那些执行事件驱动的任务来说是必不可少的提高性能的函数.如果你在使用scroll, resize, key*等事件触发执行任务时不使用降频函数,也行你就犯了重大的错误.下面这个降频函数 debounce 能让你的代码变的高效: // 返回一个函数,that, as long as it continues to be invoked, will not // be triggered. The function will be c