以前咱们讨论过宏观层面上的JavaScript
性能问题,讨论了asm.js
、WebAssembly
和WebWorker
技术,接下来咱们探究一下JavaScript
在微观层面上的性能问题,并逐步了解这些性能问题是否真实存在,以及是否须要花大量时间去优化。node
若是咱们要测试一段代码的运行速度(执行时间),咱们一般第一时间会想到编写如下代码进行测试:git
var start = Date.now()
// do something
console.log('用时:' + (Date.now() - start))
复制代码
这在很长一段时间里,我都认为这段代码能测试出绝大数多正确的结果,而事实上这段代码的结果很是不许确程序员
基于以上自写测试用例的弊端,咱们首先须要作的是重复,简单的说,就是用循环把测试代码包起来,但这并非一个简单的循环屡次求平均值的过程,相关的考虑因素还有定时器精度,结果分布状况等。可靠的测试应该结合统计学的合理实践,因此在本身没有更好的解决方法以前,选用成熟的测试工具是一个正确的决定,Benchmark.js
就是一个这样的js库。es6
npm
方式安装benchmark
github
npm install benchmark --save
复制代码
编写一个测试文件web
// index.js
var Benchmark = require('benchmark');
function foo () {
var arr = new Array(10000)
for(var i = 0;i < arr.length;i++) {
arr[i] = 0
}
}
var bench = new Benchmark(
'foo test', // 测试名
foo, // 测试内容
{
setup: `console.log('start')`, // 每一个测试循环开始时调用
teardown: `console.log('over')` // 每一个测试循环结束时调用
}
)
bench.run() // 开始测试
console.log(bench.hz) // 每秒运行数
console.log(bench.stats.moe) // 出错边界
console.log(bench.stats.variance) // 样本方差
复制代码
第三个参数中的setup
和teardown
是咱们尤为要注意的,第三个参数指定测试用例的一些额外信息,其中的setup
表示每一个测试周期开始时执行的方法,能够只是方法体,也能够是指定方法,teardown
表示每一个测试周期结束时执行的方法,类型同上。也就是运行上面的代码setup
不止执行一次,具体执行次数由Benchmark.prototype.circle
决定。chrome
好比在一次测试环境中,测试运算A每秒可运行10 000 000
次,运算B每秒可运行8 000 000
,这只能在数学意义上来说B比A慢了20%
。 咱们换个比较方法,从上面的结果不难推出A单次运行须要100ns
,听说人眼一般能分辨100ms
如下的事件,人脑能够处理的最快速度是13ms
。也就是运算A要运行650 000
次才能有但愿被人类感知到,而在web
应用中,几乎不多会进行相似的操做。 比较这么微小的差别和比较++a
a++
在性能上的差别同样,意义不大。npm
因为引擎优化的存在,因此你不能肯定一个运算A是否始终比运算B快,下面的代码浏览器
var a = '12'
// 测试1
var A = Number(a)
// 测试2
var B = parseInt(a)
复制代码
这段代码想比较Number
和parseInt
在类型转换上的性能差别,可是因为引擎优化的存在,这种测试会变得没有参考性,因为引擎优化没有被归入es的规范内容,可能有些引擎在运行测试代码的时候进行了启发式优化,它发现A和B都没有在后续被使用,因此在整个测试中实际上什么事情都没有发生,而在真实环境中,可能又并不是如此。因此咱们必须让测试环境更可能的接近真实环境。性能优化
不少状况下须要测试不一样环境下的代码运行状况,好比在chrome
和在手机版chrome
中的结果对比,在满电手机和电量2%
如下手机的运行结果对比。jsPerf.com
是一个共享测试用例和测试结果的平台。
程序员们浪费了大量的时间用于思考,或担忧他们的程序中非关键部分的速度,这些针对效率的努力在调试和维护方面带来了强烈的负面效果。咱们应该在,好比说97%的时间里,忘掉小处的效率:过早优化是万恶之源。但咱们不该该错过关键的3%的机会。 《计算访谈6》
不该该在非关键部分花太多时间,好比你的应用是一个动画表现的应用,就应该重点优化动画循环相关的代码。
// 测试1
var x = [1,2,3,4,5]
x.sort()
// 测试2
var x = [1,2,3,4,5]
x.sort(function (a,b) {
return a - b
})
复制代码
这两个测试对比sort(..)
内建方法和自定义方法的性能,可是这建立的了一个不公平的对比:
18
排在2
前面。// 测试1
var x = false;
var y = x ? 1 : 2;
// 测试2
var x;
var y = x ? 1 : 2;
复制代码
上面这个测试若是是想比较Boolean
值强制类型转换对性能的影响,那么就建立了一个不公平的对比,由于测试2少作了x
的赋值操做。要消除这个影响,应该这样作:
// 测试2
var x = undefined;
var y = x ? 1 : 2;
复制代码
最后咱们来实际测试一下,在for
循环中是否须要预先将arr.length
设定好
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite; // Benchmark.Suite是用来比较多个测试用例的类
var arr = new Array(1000)
suite.add('len', function () { // 添加一个测试用例
for (var i = 0; i < arr.length; i++) {
arr[i] = 1
}
}, {
setup: function () {
arr = new Array(1000)
}
})
.add('preLen', function () {
for (var i = 0, len = arr.length; i < len; i++) {
arr[i] = 1
}
}, {
setup: function () {
arr = new Array(1000)
}
})
.run()
console.log(suite[0].hz)
console.log(suite[1].hz)
// 1160748.8603394227 // 1188525.8945115102 // 1182959.0564495493
// 1167161.734632912 // 1196721.6273367293 // 1195146.3296931305
复制代码
以上代码的测试环境为nodejs@v8.11.4
,测试结果能够看出将arr.length
提早保存反而会形成反优化,其实背后的缘由就是在v8
等现代JavaScript
引擎中对这种循环已经作过优化,不会在每次循环都会去访问arr.length
,因此开发者再也不须要考虑这方面的问题,不要想在这方面能比引擎更聪明,结果只会拔苗助长。
es规范一般不会涉及性能方面的要求,但es6
中有一个例外,那就是尾调用优化(Tail Call Optimization
, TCO
),简单的说,尾调用就是在一个函数的末尾进行的函数调用。
在递归中,尾调用优化可能起到很是重要的做用
// 非尾调用
function foo () {
foo()
}
// 非尾调用
function foo () {
return 1 + foo()
}
// 尾调用
function foo () {
return foo()
}
复制代码
调用一个新的函数须要额外预留一块内存来管理调用帧,称为栈帧
,在没有TCO
的递归调用中,递归层级太多会致使栈溢出,递归没法运行。而在支持TCO
的环境并正确书写TCO
规范的递归函数,第二层的递归函数中直接使用上层函数的栈帧,依次类推。这样不只速度快,也更节省内存。
感谢评论区大佬的指正,TCO虽然是es6的一部分,但实质是个很是有争议的提案,主流浏览器几乎没有实现它,chrome实现过一段时间,chrome已经弃用 ,这是一份支持尾调用优化的引擎列表compat-table,能够看到Safari@12
支持尾调用优化,有兴趣的小伙伴能够去验证一下。 递归一般是堆栈溢出的“高发区”,咱们能够将递归改成循环的方式避免使用递归
"use strict"
var a = 0,
b = 0;
function demo () {
a++
if (a < 100000) {
return demo()
}
return a
}
setTimeout(() => {
console.log('递归: ' + demo())
},1000)
function demo2 () {
b++
if (b < 100000) {
return function () {
return demo2()
}
}
return b
}
function runner (fn) {
let val = fn()
while (typeof val == 'function') {
val = val()
}
return val
}
setTimeout(() => {
console.log('循环:' + runner(demo2))
})
复制代码
咱们可使用nvm
安装node@6.2.0
使用--harmony_tailcalls
参数体验尾调用优化 上面的代码运行结果