许多Javascript引擎都是为了快速运行大型的JavaScript程序而特别设 计的,例如Google的V8引擎(Chrome浏览器,Node均使用该引擎)。在开发过程当中,若是你关心你程序的内存和性能的话,你应该了解并意识 到,在你的代码背后,浏览器的JavaScript引擎中到底发生了什么事情。javascript
不论的V8,SpiderMonkey(Firefox)、Carakan(Opera)、Chakra(IE)或者其它类型的引擎。了解引擎背后 的一些运行机制能够帮助你更好的优化你的应用程序。这并非意味着你只为一种浏览器或者一种引擎进行程序的优化,并且,永远不要这样作。html
然而,你应该问本身下面这些问题:html5
在咱们编写高效、快速的代码的时候,有许多常见的陷阱。在这篇文章当中,咱们会去探索一些方法,让你的代码拥有更加良好的性能,咱们也会为这些代码提供测试样例。java
虽然在没有完全了解JavaScript引擎的状况下,开发出大型的应用程序是有可能的,这就像车主开过车却没有看过引擎盖背后的东西同样。把我选择的Chrome浏览器做为例子,我将会谈谈它的JavaScript引擎的工做机制。V8引擎,是由几个核心的部分组成的。node
● 一个基本的编译器(basecompiler),在你的代码运行以前,它会分析你的JavaScript代码而且生成本地的机器码,而不是经过字节码的方式来运行,也不是简单地解释它。这种机器码起初是没有被高度优化的。git
● V8经过对象模型(objectmodel)来表达你的对象。对象是在JavaScript中是以关联数组的方式呈现的,可是在V8引擎中,它们是经过隐藏类(hiddenclasses)的方式来表示的。这是一种能够优化查找的内部类型机制(internaltypesystem)。程序员
● 一个运行期剖析器(runtimeprofiler),它会监视正在运行的系统,而且标识出“热点”函数(“hot”function),也就是那些最后会花费大量运行时间的代码。github
● 一个优化编译器(optimizingcompiler),从新编译并优化运行期剖析器所标识“热点”代码,而后执行优化,例如,把代码进行内联化(inlining)(也就是在函数被调用的地方用函数主体去取代)。web
● V8引擎支持逆优化(deoptimization),意味着若是优化编译器发如今某些假定的状况下,把一些已经优化的代码进行了过分的优化,它就会把它门从生成的代码中抽离出来。chrome
● V8拥有垃圾回收器。理解它是如何运做的和理解如何优化你的JavaScript代码同等重要。
垃圾回收
垃圾回收是一种内存管理机制。垃圾回收器的概念是,它会尝试去从新分配已经不须要的对象所占据的内存空间。在如JavaScript拥有垃圾回收机制的语言中,若是你的程序中仍然存在指向一个对象的引用,那么该对象将不会被回收。
在大多数的状况下,咱们没有必要去手动得解除对象的引用(de-referencing)。只要简单地把变量放在它们应该的地方(在理想的状况下,变量应该尽可能为局部变量,也就是说,在它们被使用的函数中声明它们,而不是在更外层的做用域),垃圾就能正确地被回收。
在JavaScript中强制进行垃圾回收是一件不可能的事情,并且你也不会想这样作。由于垃圾回收的过程是由运行期所控制的,回收器一般知道垃圾回收的最佳时机在何时。
关于解除引用的误解
在网上很多关于JavaScript的内存分配问题的讨论中,关键字delete被频繁提出。虽然它本意是用来删除映射(map)中的键 (keys),可是很多的开发者认为也可使用它来强制解除引用。在可能的状况下,尽可能避免使用delete。在下面的例子中,删除o.x在的代码背后会 发生一些弊大于利的事情,由于它会改变o的隐藏类,而且把它转化成通常的对象,而这些通常对象会更慢。
1
2
3
|
var o = { x: 1 };
delete o.x; // true
o.x; // undefined
|
也就是说,在如今流行的JavaScript库中,你几乎确定能找到delete删除引用的身影——它也确实存在这个语言目的。这里提出来的主旨 是,让你们尽可能避免在运行期改变热点对象(hotobjects)的结构。JavaScript引擎能够检测出这种的“热点”对象并尝试去优化它们,若是 在对象的生命期中没有遇到重大的结构改变,引擎的检测和优化过程会来得更加容易,而使用delete则会触发对象结构上的这种改变。
很多人对null的使用上也存在误解。将一个对象的引用设为null,并非意味着“清空”该对象,而是将该引用指向null。用o.x=null比用delete要好,但这甚至可能不是必要的。
1
2
3
4
|
var o = { x: 1 };
o = null;
o; // null
o.x // TypeError
|
若是被删除的引用是指向对象的最后一个引用,那么该对象就知足了垃圾回收的资格。若是该引用不是指向对象的最后一个引用,那么该对象仍然能够被获取,而不会被垃圾回收。
另外要重点注意的是,要意识到,在你页面的生命期中,全局变量不会被垃圾回收器所清理。只要你的页面保持打开状态,JavaScript运行期中的全局对象就会常驻在内存当中。
1
|
var myGlobalNamespace = {};
|
只有当你刷新页面,导航到不一样的页面,关闭选项卡,或关闭你的浏览器,全局变量才会被清理。当函数做用域变量超出做用域范围,它就会被清理。当函数彻底结束,而且再没有任何引用指向其中的变量,函数中的变量会被清理。
经验法则
为了给垃圾回收器尽早,尽可能多地回收对象的机会,不要保留你再也不须要的对像。这种状况大多会自动发生;这里有几件事是要谨记的:
● 就像以前所说的那样,一个比手动解除引用更好的选择是,在恰当的做用域中使用变量。也就是说,用能够自动从做用域中剔除的函数局部变量,去取代要手动清空的全局变量。这意味着你的代码会更加的整洁且要担心的事情会更少。
● 确保要及时注销掉你再也不须要的监听事件。特别是对那些必然要删除的DOM对象。
● 若是你正在使用本地数据缓存的话,确保要清除数据缓存或者使用老化机制(agingmechanism),以避免保存了大量你不大可能复用的数据。
函数
接下来,让咱们看看函数。正如咱们所说的,垃圾回收是经过从新分配已经没法经过引用得到的内存块(对象)来工做的。为了更好地说明这一点,这里有一些例子。
1
2
3
4
|
function foo() {
var bar = new LargeObject();
bar.someCall();
}
|
当foo函数结束的时候,bar指向的对象就会自动地被垃圾回器所获取,由于已经没有任何引用指向该对象了。
对比如下代码:
1
2
3
4
5
6
7
8
|
function foo() {
var bar = new LargeObject();
bar.someCall();
return bar;
}
// somewhere else
var b = foo();
|
如今咱们有了一个指向该对象的引用,这个引用会在该次调用中保留下来,直到调用者将b赋值给其余东西(或者b超出了做用域范围)。
闭包
如今咱们来看看一个返回内部函数的函数,那个内部函数能够访问到更外层的做用域,即便外部函数已经执行完毕。这基本上就是一个闭包——一种可使用设置在特殊上下文中的变量的表现。例如:
1
2
3
4
5
6
7
8
9
10
11
|
function sum (x) {
function sumIt(y) {
return x + y;
};
return sumIt;
}
// Usage
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Returns 7
|
在sum运行上下文中创造的函数对象不会被垃圾回收,由于它被一个全局变量所指向,仍然很是容易被访问到。它能够经过sumA(n)来运行。
让咱们来看另一个例子。这里,咱们能够访问到largeStr吗?
1
2
3
4
5
6
|
var a = function () {
var largeStr = new Array(1000000).join('x');
return function () {
return largeStr;
};
}();
|
答案是确定的,咱们能够经过a()来访问到它,因此它不会被回收。咱们看看这个会怎么样:
1
2
3
4
5
6
7
|
var a = function () {
var smallStr = 'x';
var largeStr = new Array(1000000).join('x');
return function (n) {
return smallStr;
};
}();
|
咱们不再能访问到它了,它会成为垃圾回收的候选对象。
定时器
最糟糕的情况之一是内存在循环中,或者在setTimeout()/setInterval()中泄露,但这至关的常见。
考虑下面的例子:
1
2
3
4
5
6
7
8
9
|
var myObj = {
callMeMaybe: function () {
var myRef = this;
var val = setTimeout(function () {
console.log('Time is running out!');
myRef.callMeMaybe();
}, 1000);
}
};
|
若是咱们这样运行:
1
|
myObj.callMeMaybe();
|
开始定时器,咱们会看到每秒钟显示“Timeisrunningout!”而后若是咱们运行下面代码:
1
|
myObj = null;
|
定时器仍然运做。myObj不会被垃圾回收,由于传入setTimout的闭包函数仍然须要它来保证正常运做。反过来,闭包函数保留了指向 myObj的引用,由于它经过myRef来获取了该对象。若是咱们把该闭包函数传入其余任何的函数,一样的事情同样会发生,函数中仍然会存在指向对象的引 用。
一样值得紧紧记住的是,在setTimeout/setInterval的调用中的引用,例如函数引用,在运行完成以前是不会被垃圾回收的。
注意性能陷阱
很重要的一点是,除非你真正须要,不然没有必要优化你的代码,这个怎么强调都不为过。在大量的微基准测试中,你能够很轻易地发现,在V8引擎中N比M更加的优化,可是若是在真实的代码模型或者在真正的应用程序中进行测试,那些优化的实际影响可能比你指望的要小得多。
假设如今咱们想要创建的一个模块:
● 经过数字ID取出本地存储的数据资源。
● 用得到的数据生成表格内容。
● 为每一个表格单元添加事件处理,每当用户点击表格单元,切换表格单元的class。
即便这个问题解决起来很直观,可是有一些困难的因素。咱们如何去存储这些数据,如何能够高效地生成一个表格并把它添加到DOM中去,如何优化地处理这个表格的事件处理?
第一个(也是幼稚的)采起的方案多是将每块可获取的数据存放在一个对象中,而后把全部对象集合到一个数组当中。有的人可能会用jQuery去循环访问数据而后把生成表格内容,而后把它添加到DOM中。最后,有的人可能会用使用事件绑定添加点击咱们须要的点击事件。
注意:这不是你应该作的事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var moduleA = function () {
return {
data: dataArrayObject,
init: function () {
this.addTable();
this.addEvents();
},
addTable: function () {
for (var i = 0; i < rows; i++) {
$tr = $('<tr></tr>');
for (var j = 0; j < this.data.length; j++) {
$tr.append('<td>' + this.data[j]['id'] + '</td>');
}
$tr.appendTo($tbody);
}
},
addEvents: function () {
$('table td').on('click', function () {
$(this).toggleClass('active');
});
}
};
}();
|
代码简单,但它完成了咱们须要的工做。
在这种状况下,咱们惟一要迭代的只是ID,在一个标准的数组当中,数字属性能够更简单地表示出来。有趣的是,直接用 DocumentFragment和原生的DOM方法生成表格内容,比你用jQuery(上面的jQuery用法)更加的优化。固然,使用事件委托一般比 为每一个td都进行事件绑定会有更好的性能。
注意jQuery内部确实使用DocumentFragment进行了优化,但在咱们的例子中,代码中在循环中调用append(),每一次调用都 要进行额外的操做,因此在这个例子中,它达到优化效果可能并不大。但愿这应该不会是一个痛处,可是必定要用基准测试来确保本身的代码没有问题。
在咱们的例子当中,添加这些以上的优化会获得一些不错(预期)的性能收益。相对于简单的绑定,事件委托提供了至关好的改进,且选择用documentFragment会是一个真正的性能助推器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
var moduleD = function () {
return {
data: dataArray,
init: function () {
this.addTable();
this.addEvents();
},
addTable: function () {
var td, tr;
var frag = document.createDocumentFragment();
var frag2 = document.createDocumentFragment();
for (var i = 0; i < rows; i++) {
tr = document.createElement('tr');
for (var j = 0; j < this.data.length; j++) {
td = document.createElement('td');
td.appendChild(document.createTextNode(this.data[j]));
frag2.appendChild(td);
}
tr.appendChild(frag2);
frag.appendChild(tr);
}
tbody.appendChild(frag);
},
addEvents: function () {
$('table').on('click', 'td', function () {
$(this).toggleClass('active');
});
}
};
}();
|
咱们可能会寻找其余的方案来提升性能。你可能在某些文章中了解到用原型模式比用模块模式更加优化(咱们不久前已经证实了事实并不是如此),或者了解到 JavaScript模板框架是通过高度的优化的。有时它们的确是这样,可是使用它们只是为了代码拥有更强的可读性。同时,还有预编译!让咱们测试一下, 实际上这有多少是能带来真正优化的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
moduleG = function () {};
moduleG.prototype.data = dataArray;
moduleG.prototype.init = function () {
this.addTable();
this.addEvents();
};
moduleG.prototype.addTable = function () {
var template = _.template($('#template').text());
var html = template({'data' : this.data});
$tbody.append(html);
};
moduleG.prototype.addEvents = function () {
$('table').on('click', 'td', function () {
$(this).toggleClass('active');
});
};
var modG = new moduleG();
|
正如结果所示,在这种状况下所带来的性能效益是微不足道的。选择模板和原型不会真正提供得比咱们原来拥有的东西更多的东西。听说,性能并非现代开发者所真正使用它们的缘由——而是它给你的代码库所带来的可读性,继承模型,以及可维护性。
更复杂的问题包括如何高效地在canvas上绘制图像,和如何使用或不使用类型数组去操做像素数据。
在你的代码使用它们以前,要给你的微基准测试一个结束前的检验。大家其中有些人可能会回想起JavaScript模板语言shoot-off和它的 以后扩展版的shoot-off。若是你想确保测试不会被现实的应用程序的中你不想见到的约束所影响——请在真实的代码中和优化一块儿测试。
V8优化技巧
同时详细的陈列每个V8的每一种优化显然超出了本文的讨论范围,其中有许多特定的优化技巧都值得注意。记住如下的一些建议你就能够减小你写出低性能的代码的机会。
● 特定的模式会致使V8放弃优化。例如使用try-catch,就会致使这种状况的发生。若是想要了解跟多关于什么函数能够被优化,什么函数不能够,你可使用V8引擎中附带的D8shell实用程序中的–trace-optfile.js。
● 若是你关心运行速度,那么就要尽可能保持你的函数的功能的单一性,也就是说,确保变量(包括属性,数组,和函数参数)永远只是相同隐藏类的包含对象。例如,永远不要干这种事:
● 不要从未初始化的或已经被删除的元素上加载内容。这样作可能对你的程序运行结果不会形成影响。可是它会使得程序运行得更慢。
● 不要写过于庞大的函数,由于他们更难被优化。
若是想知道更多的优化技巧,能够观看DanielClifford的GoogleI/O大会上的演讲BreakingtheJavaScriptSpeedLimitwithV8,它同时也涵盖了上面咱们所说的优化技巧。OptimizingForV8—ASeries也一样值得一读。
对象和数组:我应该用哪个?
● 若是你想存储一组数字,或者一系列的同类型对象的话,那么就使用数组。
● 若是你想要的是一个语义上的有不一样属性(不一样类型)的对象,那么就使用包含属性的对象。这样从内存上来讲会至关的高效,并且运行也至关的迅速。
● 用整数作索引的元素,无论它们是存储在数组仍是对象中,都会比那些需经过迭代来获取的对象属性要快得多。
● 对象中的属性至关复杂:它们能够被setter所建立,拥有不一样的可枚举性和可写性。数组中的元素不能有这样的定制性——它们只有存在或者不存在的状态。 在一个引擎的层面,从组织表示结构的内存角度上来讲,这容许有更多的优化。当一个数组中包含有数字的时候,这样会至关有好处。例如,当你须要一个向量,不 要用一个包含有x,y,z属性的对象,用一个数组来存储就能够了。
使用对象的技巧
● 用构造函数构造对象。这样能够保证全部的由该构造函数构造的对象都具备相同的隐藏类,并且能够有助于避免修改这些隐藏类。有个附加的好处就是,它会比Object.create()稍快。
● 在你的程序中,对象的类型数目以及它们的复杂程度是没有限制的(不难理解的是:长原型链会可能会致使有害的结果,那些只有少数属性的对象的特殊表现就是,它们会比那些更大的对象运行得要快一点)。对于“热点”对象,尽可能保持原型链的简短,以及属性数目较少。
对象的复制
对于应用的开发者来讲,对象的复制是一个常见的问题。虽然基础测试可能代表V8在不一样的状况下对这类问题都处理得很好,可是,当你要复制任何东西的 时候,仍然须要当心。复制大的东西一般是缓慢的——因此不要这样作。JavaScript中的for…in循环处理这种事情特别的糟糕,由于它拥有可怕的 规范,这使得它在任何引擎中处理任何对象,都不会得到良好的速度。
当你必定要在一段性能要求苛刻的代码中复制对象(而且你没法摆脱这种情况),那么就用数组或者一个自定义的“拷贝构造函数”,帮你逐一明确地复制对象的每个属性。这多是实现的最快的方式:
1
2
3
4
5
|
function clone(original) {
this.foo = original.foo;
this.bar = original.bar;
}
var copy = new clone(original);
|
模块模式中的缓存函数
在模块模式中缓存你的函数能够带来性能的提升。看看下面的例子,由于它老是会强制进行成员函数的复制,你习惯看到的变化可能会更慢。
这里有个关于原型对比模块模式的性能测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
// Prototypal pattern
Klass1 = function () {}
Klass1.prototype.foo = function () {
log('foo');
}
Klass1.prototype.bar = function () {
log('bar');
}
// Module pattern
Klass2 = function () {
var foo = function () {
log('foo');
},
bar = function () {
log('bar');
};
return {
foo: foo,
bar: bar
}
}
// Module pattern with cached functions
var FooFunction = function () {
log('foo');
};
var BarFunction = function () {
log('bar');
};
Klass3 = function () {
return {
foo: FooFunction,
bar: BarFunction
}
}
// Iteration tests
// Prototypal
var i = 1000,
objs = [];
while (i--) {
var o = new Klass1()
objs.push(new Klass1());
o.bar;
o.foo;
}
// Module pattern
var i = 1000,
objs = [];
while (i--) {
var o = Klass2()
objs.push(Klass2());
o.bar;
o.foo;
}
// Module pattern with cached functions
var i = 1000,
objs = [];
while (i--) {
var o = Klass3()
objs.push(Klass3());
o.bar;
o.foo;
}
// See the test for full details
|
使用数组的技巧
接下来咱们来关谈论一下关于数组的一些技巧。一般状况下,不要删除数组的元素。不然会使得数组内部表现形式发生转变,从而变得更慢。当键变得稀疏的时候,V8会最终把元素转换成更慢的字典模式。
数组字面量
用数组字面量建立数组是有用的,由于它们会给VM一些暗示,让它知道数组的大小和类型。字面量一般对规模不大的数组是好处的。
1
2
3
4
5
6
7
8
|
// Here V8 can see that you want a 4-element array containing numbers:
var a = [1, 2, 3, 4];
// Don't do this:
a = []; // Here V8 knows nothing about the array
for(var i = 1; i <= 4; i++) {
a.push(i);
}
|
存储单一类型VS混合类型
在同一个数组中存储不一样类型的数据(例如,数字,字符串,undefined,或者true/false),历来不是一个好主意(也就是像这样,vararr=[1,“1”,undefined,“true”])。
咱们能够从结果中看出,ints数组是最快的。
稀疏数组VS满数组
当你使用稀疏数组的时候,要意识到,在它们中访问元素的效率要比在满数组中要慢得多。这是由于若是数组中只有少数元素,V8不会为元素重新分配连续的内存空间。它们会被一个字典所管理,这样能够节约内存空间,可是会消耗访问时间。
满数组的加法和无0的数组的加法其实是最快的。而一个满数组中是否含有0对它的运行效率没有影响。
塞满的数组VS多孔的数组
避免数组中的“孔”(可能经过删除元素或者用a[x]=foo,而x>a.length来建立的孔)。在一个“满”的数组中,即便是仅仅是一个元素被删除掉,也会变得慢得多。
预分配数组VS运行时分配
不要根据数组最大的大小预分配一个大数组(例如大于64K的元素),应该让你的数组在运行的时候自我分配。在咱们进行对这个技巧的性能测试以前,请记住,这只适合部分JavaScript浏览器。
在Nitro引擎(Safari)使用预分配的数组会更有好处。可是,在其余的引擎中(V8,SpiderMonkey),非预分配会更高效。
1
2
3
4
5
6
7
8
9
10
11
|
// Empty array
var arr = [];
for (var i = 0; i < 1000000; i++) {
arr[i] = i;
}
// Pre-allocated array
var arr = new Array(1000000);
for (var i = 0; i < 1000000; i++) {
arr[i] = i;
}
|
优化你的应用程序
在web应用程序的世界里面,速度是一切。没有用户但愿一个电子表格程序须要几秒钟的时间去计算一列数据的和,或者用一分钟的时间去获得表格的汇总信息。这就为何你须要压榨你代码中的每一点的性能的缘由,这有时可能会苛刻。
虽然理解和提升你的应用程序性能是有用的,可是它依然很困难。咱们给出下面几步建议去解决你程序性能的瓶颈:
● 测量它:找出你应用程序中慢的地方(~45%)
● 理解它:发现实际的问题是什么(~45%)
● 解决它!(~10%)
一些推荐的工具和技术能够协助你进行这个过程。
基准测试
有许多方法能够测试JavaScript代码片段的性能——通常的设想是:基准测试就是简单地对两个时间戳进行比较。这样的模式已经被jsPerf团队所指出,而且应用在在SunSpider和Kraken的基准测试套件当中。
1
2
3
4
5
6
7
8
9
|
var totalTime,
start = new Date,
iterations = 1000;
while (iterations--) {
// Code snippet goes here
}
// totalTime → the number of milliseconds taken
// to execute the code snippet 1000 times
totalTime = new Date - start;
|
这里,测试代码被放在一个循环当中,而且运行一个设定的次数(例如六次)。接下来,用结束的时间去减开始开始的时间。这样,就能够测试出循环中操做所消耗的时间。
然而,这里对基准测试的工做过分简化了,尤为是若是你想在多个浏览器和环境中进行基准测试。垃圾回收自己会影响到你的测试结果。即便你使用了像window.preformance这样的解决方案,你仍然须要对那些陷阱作出相应的考虑。
无论你是简单地对你部分的代码进行基准测试,仍是编写一个测试套件,或写一个基准测试的类库。关于JavaScript基准测试实际要作的事情比你 想象的要多。若是你想得到关于基准测试更多的细节指导,我强烈建议你阅读MathiasBynens和John-DavidDalton编的JavaScriptBenchmarking
性能分析
Chrome的开发者工具提供了对JavaScript性能分析良好的支持。你可使用这些特性去检测那些函数消耗了你大部分的性能,而后你就能够对它们进行优化。这是很重要的一点,由于即便你的代码库中一点很小的改变均可以影响到你的总体性能。
性能分析以获取你代码当前性能的基线开始,你能够从时间轴上发现它。它会告诉你你的代码花费了多长的运行时间。Profiles选项卡提供了一个更 好的视角去观察咱们的应用程序内部到底放生了什么事情。JavaScriptCPUprofile展现了咱们的代码到底占用了多少CPU的时 间,CSSselectorprofile告诉咱们选择器查找元素所花费的时间,Heapsnapsshots让咱们知道咱们的对象占用了多少内存。
使用这些工具,咱们能够抽离,调整和从新分析来度量咱们的对特定函数或操做的改变是否真正起到了性能优化的效果。
想获得关于性能分析更好的介绍,能够阅读ZackGrossbart的JavaScriptProfilingWithTheChromeDeveloperTools,
提示:在理想状况下,若是你想保证的你性能分析没有受到你所安装的扩展程序或者应用所影响。以能够用usingthe–user-data- dir<empty_directory>的flag来运行Chrome。在大多数状况下,这样的性能测试已经足够了,可是有些时候你会须要 的更多。V8的flags就能够为你提供帮助。
避免内存泄露——用三快照(THREESNAPSHOT)技术发现问题
在Google内部,Chrome的开发者工具被例如Gmail这样的团队大量使用,能够帮助咱们发现并解决内存泄露问题。
咱们团队关注的一些内存数据,包括私有内存使用,JavaScript堆大小,DOM节点数目,内存清理,事件监听数目和垃圾回收器的运行状况。如 果你对事件驱动架构比较熟悉,你或许比较有兴趣了解最多见问题之一就是,咱们过去经常用listen(),unlisten()(闭包),和缺失的 dispose()去处理事件监听对象。
幸运的是,DevTools能够帮咱们解决其中的一些问题,强烈建议阅读LoreenaLee的很棒的一个展现,它记录了如何使用“三快照技术”找出DevTools中的内存泄露。
这项技术的要点是,记录你应用程序中的一些行为,强制进行垃圾回收,检测DOM的节点数目是否会返回到你所指望的基线,而后分析三个堆上的快照来决定内存是否泄露。
单页面程序的内存管理
内存管理对于现代的单页面应用程序(例如AngularJS,Backbone,Ember)至关的重要,由于它们几乎历来不会刷新页面。这就意味 着内存泄露会至关的明显和迅速。这在移动单页面应用程序中存在至关大的陷阱,由于内存的限制,且存在大量的例如email客户端这样的长期运行的程序或者 社交网络应用程序。能力越大,责任越大。(推荐阅读:《每位开发人员都应铭记的10句编程谚语》)
有许多方法能够避免这中状况。在Backbone中,确保你老是用dispose()(当前能够在Backbone(edge)中使用)来处理了旧 的视图和引用。这个函数是最近新增的,能够移除任何添加到视图的“event”对象的全部处理函数,以及视图做为第三个参数(回调上下文)传入的任何集合 或者模型的事件监听器,dispose()一样能够被视图的remove()所调用,当元素从页面中被移除时管理主要的基本内存清理工做。其余的类库,如 Ember,当它检测到元素已经从视图中被移除的时候,它会移除相应的观察者,以免内存泄露。
DerickBailey给了咱们一些明智的建议:
“除了了解事件处理在引用层面是如何工做的,在JavaScript按照标准规则来管理你的内存,一切就会没问题。若是你想向一个塞满用户数据的 Backbone集合中加载数据,若是你想那些集合稍后被清理并不占据内存,你必须移除集合全部的引用以及其中独立的对象。一旦你移除全部的引用,清理工 做就能够进行。这就是标准的JavaScript垃圾回收规则”
在这篇文章中,Derick涵盖了在使用Backbone.js中许多常见的内存陷阱以及解决方案。
这里也有一篇由FelixGeisendrfer编写关于如何调试Node中的内存泄露的指导,很值得一读,尤为它造成了你的更普遍SPA堆里面的一部分。
最小化重排
当浏览器为了重绘(re-rendering)必须从新计算元素的位置和几何形状的时候,咱们把这个过程称做重排(reflow)。在浏览器中,重排是一个用户阻塞的操做,因此了解如何去提升重排的性能会颇有帮助。
你应该用批量处理的方法去触发重排或重绘,而且要有节制地使用这些方法。尽可能不进行DOM操做也很重要。用轻量级的DocumentFragment(文本片断)对象来达到这样的效果。你能够把它当作是获取部分DOM树的方法,或者是建立新的文本“片断”的方法。对比不断地向DOM树添加节点,咱们使用文本片断构建起咱们须要的内容而且只进行一次DOM插入操做。这样能够避免过分的重排。
例如,咱们写了一个向一个元素中添加20个div函数。简单地添加每一个div到元素中会触发20次重排。
1
2
3
4
5
6
7
8
|
function addDivs(element) {
var div;
for (var i = 0; i < 20; i ++) {
div = document.createElement('div');
div.innerHTML = 'Heya!';
element.appendChild(div);
}
}
|
为了解决这个问题,咱们使用DocumentFragment来代替逐个添加div。使用咱们像appendChild这样的方法把DocumentFragment添加到元素中的时候,文本片断中全部的子节点都会被添加到元素中,这样只会触发仅仅一次重排。
1
2
3
4
5
6
7
8
9
10
11
|
function addDivs(element) {
var div;
// Creates a new empty DocumentFragment.
var fragment = document.createDocumentFragment();
for (var i = 0; i < 20; i ++) {
div = document.createElement('a');
div.innerHTML = 'Heya!';
fragment.appendChild(div);
}
element.appendChild(fragment);
}
|
你能够在MaketheWebFaster,JavaScriptMemoryOptimization和FindingMemoryLeaks.阅读到更多关于这方面的主题。
JavaScript内存泄露检测器
为了帮助发现JavaScript内存泄露,我两个谷歌的同事(MarjaHlttandJochenEisinger)开发了一个和Chrome开发者工具共同使用的工具(具体来讲,就是一个远程检测协议),能够检索堆快照和探测出哪些对象致使了内存泄露。
有一篇文章完整地介绍了怎么使用这个工具,我鼓励你去看看或者去看LeakFinder的项目主页。
更多信息:也许你想知道为何这个工具没有集成到咱们的开发者工具当中。有两个缘由,第一,它最初是为了帮助咱们在Closure库中为咱们检测特 定的内存场景。第二,它做为一个扩展工具来使用将会更有意义(甚至能够做为一个扩展程序,若是咱们能够适当地得到堆性能分析扩展程序的API的话)。
V8标签:调试性能&垃圾回收
Chrome支持经过js-flags直接传入一些标签到V8引擎中来获取关于引擎优化过程当中更多细节。例如,这能够追溯V8的优化:
1
|
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"
|
Windows用户能够运行chrome.exe–js-flags=”–trace-opt–trace-deopt”
在开发你的应用程序的过程当中,这些V8标签会有所帮助:
● trace-opt–记录已经被优化的函数,已经显示优化器没法识别而且跳过的代码。
● trace-deopt–记录运行过程当中须要逆优化的代码列表。
● trace-gc–记录每次进行垃圾回收的跟踪线。
V8的tick-processing脚本用*标注已经进行过优化的函数,用~标记没有被优化的函数。
若是你想了解更多关于V8引擎标签以及V8内部工做原理的话,我强烈建议你浏览一篇关于V8引擎内部工做原理的文章,它总结了一些目前为止最好的资源。
高精度时间以及导航计时API
高精度时间(HRT)是一个不受系统时钟以及用户调整影响的亚毫秒级的JavaScript接口。它提供了比newDate和Date.now()更为精准的时间测量。这样能够帮助咱们写出性能良好的基础测试。
HRT目前在Chrome(稳定版)中能够经过window.performance.webkitNow()来得到,可是前缀在 ChromCanary中被省略了,能够经过window.performance.now()来获取。PaulIrish在HTML5Rocks中写了 一篇关于HRT的文章。
因此,咱们如今知道了目前的时间,可是若是咱们须要API给出更精确的时间去测量web中的性能呢?
如今,咱们有个NavigationTimingAPI的东西可使用。这个API提供了一个简单的方法去获取当页面加载完毕并展现给用户时的精确和详细的时间测量。时间信息能够经过window.performance.timing暴露出来,你能够在控制台中简单地使用它:
观察上面的数据,咱们能够抽离出一些至关有用的信息。例如,网络延迟为responseEnd-fetchStart,从服务器加载页面时间为 loadEventEnd-responseEnd,以及导航和页面加载之间的的耗时为loadEventEnd-navigationStart。
正如你所看到的,一个performance.memory属性一样能够提供例如总堆大小的JavaScript内存使用状况。
关于导航计时API更多的细节,你能够阅读SamDutton的一篇至关好的文章MeasuringPageLoadSpeedWithNavigationTiming.
ABBOUT:MEMORY和ABOUT:TRACING
Chrome中的about:tracing提供了有效的视图,帮助咱们观察浏览器的性能,记录Chrome如每一个线程,选项卡,和进程的全部活动。
这个工具真正的有用的地方是能够容许你获取Chrome的浏览器引擎盖下的分析数据,而后你能够恰当地调整你的JavaScript程序,或者优化你资源加载过程。
LilliThompson有一篇写给游戏开发者的文章,关于如何使用about:tracing去分析WebGL游戏。这篇文章对于普通的JavaScripters依然适用。
Chrome中使用about:memory也颇有帮助,由于它显示了每一个选项卡精确的内存使用,这样能够有效的跟踪潜在的内存泄露。
总结
正如咱们所见,在JavaScript的引擎世界里面,有许多的隐藏的性能陷阱,事实上并无性能提升的银弹。只有当你在(现实世界的)测试环境中结合一系列的优化,你才会意识到最大的性能获益。可是即便这样,理解引擎内部原理以及优化你的代码能够给你提升你应用程序性能的灵感。
测量它,理解它,解决它。不断重复这个过程。
记得关注优化,但要避免一些小的优化从而得到更大的便利。例如,一些开发者在循环中为便利而是用.forEach和Object.keys来代替 for和forin,即便它们更慢,可是在能够接受范围内,它们更为方便。你须要一个清醒的头脑去分析你的应用程序中哪些是须要优化的,哪些是不须要的。
一样,意识到即便JavaScript引擎不断变得更快,一个真证的瓶颈实际上是DOM。重排和重绘的最小化是至关重要的,因此记住若是不是非不得已,不要触碰DOM。而且关注网络状况。HTTP请求是珍贵的,尤为是在移动终端,你应该使用HTTP缓存来减小资源的消耗。
记住全部这一切能够确保你已经得到这篇文章的大部分信息。我但愿这会对你有所帮助!