在软件中,性能一直扮演着重要的角色。在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. var hash = 0;
  3. if (source.length === 0) return hash;
  4. for (var i = 0; i < source.length; i++) {
  5. var char = source.charCodeAt(i);
  6. hash = ((hash<<5)-hash)+char;
  7. hash = hash & hash; // Convert to 32bit integer
  8. }
  9. return hash;
  10. }

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

  1. var t0 = performance.now();
  2. var result = makeHash('Peter');
  3. var t1 = performance.now();
  4. 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. console.log(makeHash('Peter')); // bad idea!
  3. var t1 = performance.now();
  4. console.log('Took', (t1 - t0).toFixed(4), 'milliseconds');

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

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

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

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

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

缺陷 #2 – 只衡量一次

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

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

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

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

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

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

缺陷 #3 – 太依赖平均值

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

  1. Took 0.2730 milliseconds to generate: 77005292
  2. Took 0.0234 milliseconds to generate: 77005292
  3. Took 0.0200 milliseconds to generate: 77005292
  4. Took 0.0281 milliseconds to generate: 77005292
  5. Took 0.0162 milliseconds to generate: 77005292
  6. Took 0.0245 milliseconds to generate: 77005292
  7. Took 0.0677 milliseconds to generate: 77005292
  8. Took 0.0289 milliseconds to generate: 77005292
  9. Took 0.0240 milliseconds to generate: 77005292
  10. Took 0.0311 milliseconds to generate: 77005292

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

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

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

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

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

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

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

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

  1. function isIn(haystack, needle) {
  2. var found = false;
  3. haystack.forEach(function(element) {
  4. if (element.toLowerCase() === needle.toLowerCase()) {
  5. found = true;
  6. }
  7. });
  8. return found;
  9. }
  10. console.log(isIn(['a','b','c'], 'B'));  // true
  11. console.log(isIn(['a','b','c'], 'd'));  // false

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

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

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

  1. function isIn1(haystack, needle) {
  2. var found = false;
  3. haystack.forEach(function(element) {
  4. if (element.toLowerCase() === needle.toLowerCase()) {
  5. found = true;
  6. }
  7. });
  8. return found;
  9. }
  10. function isIn2(haystack, needle) {
  11. for (var i = 0, len = haystack.length; i < len; i++) {
  12. if (haystack[i].toLowerCase() === needle.toLowerCase()) {
  13. return true;
  14. }
  15. }
  16. return false;
  17. }
  18. console.log(isIn1(['a','b','c'], 'B'));  // true
  19. console.log(isIn1(['a','b','c'], 'd'));  // false
  20. console.log(isIn2(['a','b','c'], 'B'));  // true
  21. console.log(isIn2(['a','b','c'], 'd'));  // false
  22. function median(sequence) {
  23. sequence.sort();  // note that direction doesn't matter
  24. return sequence[Math.ceil(sequence.length / 2)];
  25. }
  26. function measureFunction(func) {
  27. 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(',');
  28. var numbers = [];
  29. for (var i = 0; i < letters.length; i++) {
  30. var t0 = performance.now();
  31. func(letters, letters[i]);
  32. var t1 = performance.now();
  33. numbers.push(t1 - t0);
  34. }
  35. console.log(func.name, 'took', median(numbers).toFixed(4));
  36. }
  37. measureFunction(isIn1);
  38. measureFunction(isIn2);

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

  1. true
  2. false
  3. true
  4. false
  5. isIn1 took 0.0050
  6. 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

测试JavaScript函数的性能相关推荐

  1. 使用timeit测试Python函数的性能

    timeit是Python标准库内置的小工具,可以快速测试小段代码的性能. 认识timeit timeit 函数: timeit.timeit(stmt, setup,timer, number) 参 ...

  2. Chrome DevTools:在 Profile 性能分析中显示原生 javascript 函数

    本文翻译自 Chrome DevTools: Show native functions in JS Profile,中文版首发在我的知乎专栏 V8 源码及周边. 在 Chrome DevTools ...

  3. 在JavaScript函数中定义全局变量

    是否可以在JavaScript函数中定义全局变量? 我想在其他函数中使用trailimage变量(在makeObj函数中声明). <html xmlns="http://www.w3. ...

  4. 如何优化JavaScript脚本的性能

    随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术.而现在Ajax则是最为流行的一种方式.JavaScript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能 ...

  5. HTML和JavaScript函数之间的关系

    在javaScript中有两类函数,一个是自定义的函数,一个是JavaScript中的函数.在HTML中javaSrcipt能和HTML完全的融合,也就是我们的JavaScript系统函数能直接的应用 ...

  6. 测试mktime和localtime_r性能及优化方法

    // 测试mktime和localtime_r性能及优化方法 // // 编译方法:g++ -g -o x x.cpp或g++ -O2 -o x x.cpp,两种编译方式性能基本相同. // // 结 ...

  7. JavaScript 函数的定义

    一.关于函数 JavaScript函数是指一个特定代码块,可能包含多条语句,可以通过名字来供其他语句调用以执行函数包含的代码语句. 比如我们有一个特定的功能需要三条语句实现 那么每次想实现这个功能的时 ...

  8. 第一百零二节,JavaScript函数

    JavaScript函数 学习要点: 1.函数声明 2.return返回值 3.arguments对象 函数是定义一次但却可以调用或执行任意多次的一段JS代码.函数有时会有参数,即函数被调用时指定了值 ...

  9. [转]WEB开发者必备的7个JavaScript函数

    我记得数年前,只要我们编写JavaScript,都必须用到几个常用的函数,比如,addEventListener 和 attachEvent,并不是为了很超前的技术和功能,只是一些基本的任务,原因是各 ...

最新文章

  1. Js时间格式[转载]
  2. 高逼格的 SQL 写法:行行比较
  3. U平方Net:深入使用嵌套的U型结构进行显著目标检测
  4. 出色图形用户界面(GUI)设计规范
  5. 二十、PHP框架Laravel学习笔记——模型的作用域
  6. linux mysql 静态库_Linux静态库与动态库实例详解
  7. 灰色预测法 —— matlab
  8. 【转载】计算机网络基础知识总结
  9. 西南交通大学计算机学院,西南交通大学2021年招生计划:四川招1830人、重庆招426人...
  10. 非线性优化_曲线拟合_Ceres_最小二乘法示例
  11. Improving Opencv 7: The Core Functionality : Discrete Fourier Transform
  12. Linux内核memcpy的不同实现
  13. 统计学(贾俊平《第七版》)知识总结
  14. 联想r720安装固态_联想LenovoR720游戏笔记本如何安装固态硬盘
  15. 早悟兰因(兰因絮果)
  16. Pt100铂电阻与惠斯通电桥
  17. 为什么企业微信只能群发一次?如何增加群发次数?
  18. 分享几个小众又高薪的职业
  19. 史玉柱和郭广昌谈创业团队管理:两种类型的“孔雀王”
  20. 版本迭代和测试周期的意思

热门文章

  1. Shell命令-磁盘与文件系统之e2fsck、mkswap
  2. [Java面经] 关于面试的二三事.
  3. div+css 布局浏览器兼容
  4. java中的泛型(E)
  5. 数仓数据分层(ODS DWD DWS ADS)
  6. Kafka万亿级消息实战解决方案干货
  7. 什么时候用到mysql存储过程_什么时候用到存储过程
  8. java作业不能运行_从Windows运行时,YARN作业失败
  9. linux下安装mysql_Linux下安装mysql-8.0.20的教程详解
  10. android代码关闭数据库,android – 我应该如何正确打开和关闭我的数据库