原文:http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/javascript
做者:Addy Osmanicss
译者按:本人第一次翻译外文,言语不免有些晦涩,但尽可能表达了做者的原意,未通过多的润色,欢迎批评指正。另本文篇幅较长、信息量大,可能难以消化,欢迎留言探讨细节问题。本文主要关注V8的性能优化,部份内容并不适用于全部JS引擎。最后,转载请注明出处: )html
========================译文分割线===========================html5
不少JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是专门为须要快速执行的大型JavaScript应用所设计的。若是你是一个开发者,而且关心内存使用状况与页面性能,你应该了解用户浏览器中的JavaScript引擎是如何运做的。不管是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其余引擎,这样作能够帮助你更好地优化你的应用程序。这并非说应该专门为某一浏览器或引擎作优化,千万别这么作。java
可是,你应该问本身几个问题:node
加载快速的网站就像是一辆快速的跑车,须要用到特别定制的零件. 图片来源: dHybridcars.git
编写高性能代码时有一些常见的陷阱,在这篇文章中,咱们将展现一些通过验证的、更好的编写代码方式。github
若是你对JS引擎没有较深的了解,开发一个大型Web应用也没啥问题,就比如会开车的人也只是看过引擎盖而没有看过车盖内的引擎同样。鉴于Chrome是个人浏览器首选,因此谈一下它的JavaScript引擎。V8是由如下几个核心部分组成:web
垃圾回收是内存管理的一种形式,其实就是一个收集器的概念,尝试回收再也不被使用的对象所占用的内存。在JavaScript这种垃圾回收语言中,应用程序中仍在被引用的对象不会被清除。chrome
手动消除对象引用在大多数状况下是没有必要的。经过简单地把变量放在须要它们的地方(理想状况下,尽量是局部做用域,即它们被使用的函数里而不是函数外层),一切将运做地很好。
垃圾回收器尝试回收内存. 图片来源: Valtteri Mäki.
在JavaScript中,是不可能强制进行垃圾回收的。你不该该这么作,由于垃圾收集过程是由运行时控制的,它知道什么是最好的清理时机。
网上有许多关于JavaScript内存回收的讨论都谈到delete这个关键字,虽然它能够被用来删除对象(map)中的属性(key),但有部分开发者认为它能够用来强制“消除引用”。建议尽量避免使用delete,在下面的例子中delete o.x 的弊大于利,由于它改变了o的隐藏类,并使它成为一个"慢对象"。
var o = { x: 1 }; delete o.x; // true o.x; // undefined
你会很容易地在流行的JS库中找到引用删除——这是具备语言目的性的。这里须要注意的是避免在运行时修改”hot”对象的结构。JavaScript引擎能够检测出这种“hot”的对象,并尝试对其进行优化。若是对象在生命周期中其结构没有较大的改变,引擎将会更容易优化对象,而delete操做实际上会触发这种较大的结构改变,所以不利于引擎的优化。
对于null是如何工做也是有误解的。将一个对象引用设置为null,并无使对象变“空”,只是将它的引用设置为空而已。使用o.x= null比使用delete会更好些,但可能也不是很必要。
var o = { x: 1 }; o = null; o; // null o.x // TypeError
若是此引用是当前对象的最后引用,那么该对象将被做为垃圾回收。若是此引用不是当前对象的最后引用,则该对象是可访问的且不会被垃圾回收。
另外须要注意的是,全局变量在页面的生命周期里是不被垃圾回收器清理的。不管页面打开多久,JavaScript运行时全局对象做用域中的变量会一直存在。
var myGlobalNamespace = {};
全局对象只会在刷新页面、导航到其余页面、关闭标签页或退出浏览器时才会被清理。函数做用域的变量将在超出做用域时被清理,即退出函数时,已经没有任何引用,这样的变量就被清理了。
为了使垃圾回收器尽早收集尽量多的对象,不要hold着再也不使用的对象。这里有几件事须要记住:
接下来,咱们谈谈函数。正如咱们已经说过,垃圾收集的工做原理,是经过回收再也不是访问的内存块(对象)。为了更好地说明这一点,这里有一些例子。
function foo() { var bar = new LargeObject(); bar.someCall(); }
当foo返回时,bar指向的对象将会被垃圾收集器自动回收,由于它已没有任何存在的引用了。
对比一下:
function foo() { var bar = new LargeObject(); bar.someCall(); return bar; } // somewhere else var b = foo();
如今咱们有一个引用指向bar对象,这样bar对象的生存周期就从foo的调用一直持续到调用者指定别的变量b(或b超出范围)。
当你看到一个函数,返回一个内部函数,该内部函数将得到范围外的访问权,即便在外部函数执行以后。这是一个基本的闭包 —— 能够在特定的上下文中设置的变量的表达式。例如:
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调用上下文中生成的函数对象(sumIt)是没法被回收的,它被全局变量(sumA)所引用,而且能够经过sumA(n)调用。
让咱们来看看另一个例子,这里咱们能够访问变量largeStr吗?
var a = function () { var largeStr = new Array(1000000).join('x'); return function () { return largeStr; }; }();
是的,咱们能够经过a()访问largeStr,因此它没有被回收。下面这个呢?
var a = function () { var smallStr = 'x'; var largeStr = new Array(1000000).join('x'); return function (n) { return smallStr; }; }();
咱们不能再访问largeStr了,它已是垃圾回收候选人了。【译者注:由于largeStr已不存在外部引用了】
最糟的内存泄漏地方之一是在循环中,或者在setTimeout()/ setInterval()中,但这是至关常见的。思考下面的例子:
var myObj = { callMeMaybe: function () { var myRef = this; var val = setTimeout(function () { console.log('Time is running out!'); myRef.callMeMaybe(); }, 1000); } };
若是咱们运行myObj.callMeMaybe();来启动定时器,能够看到控制台每秒打印出“Time is running out!”。若是接着运行myObj =
null,定时器依旧处于激活状态。为了可以持续执行,闭包将myObj传递给setTimeout,这样myObj是没法被回收的。相反,它引用到myObj的由于它捕获了myRef。这跟咱们为了保持引用将闭包传给其余的函数是同样的。
一样值得牢记的是,setTimeout/setInterval调用(如函数)中的引用,将须要执行和完成,才能够被垃圾收集。
永远不要优化代码,直到你真正须要。如今常常能够看到一些基准测试,显示N比M在V8中更为优化,可是在模块代码或应用中测试一下会发现,这些优化真正的效果比你指望的要小的多。
作的过多还不如什么都不作. 图片来源: Tim Sheerman-Chase.
好比咱们想要建立这样一个模块:
这个问题有几个不一样的因素,虽然也很容易解决。咱们如何存储数据,如何高效地绘制表格而且append到DOM中,如何更优地处理表格事件?
面对这些问题最开始(天真)的作法是使用对象存储数据并放入数组中,使用jQuery遍历数据绘制表格并append到DOM中,最后使用事件绑定咱们指望地点击行为。
注意:这不是你应该作的
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(以这种方式)来生成表格是更优的选择,固然,事件代理比单独绑定每一个td具备更高的性能。
要注意虽然jQuery在内部使用DocumentFragment,可是在咱们的例子中,代码在循环内调用append而且这些调用涉及到一些其余的小知识,所以在这里起到的优化做用不大。但愿这不会是一个痛点,但请务必进行基准测试,以确保本身代码ok。
对于咱们的例子,上述的作法带来了(指望的)性能提高。事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推做用。
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'); }); } }; }();
接下来看看其余提高性能的方式。你也许曾经在哪读到过使用原型模式比模块模式更优,或据说过使用JS模版框架性能更好。有时的确如此,不过使用它们实际上是为了代码更具可读性。对了,还有预编译!让咱们看看在实践中表现的如何?
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上绘制图片和操做带或不带类型数组的像素数据。
在将一些方法用在你本身的应用以前,必定要多了解这些方案的基准测试。也许有人还记得JS模版的shoot-off和随后的扩展版。你要搞清楚基准测试不是存在于你看不到的那些虚拟应用,而是应该在你的实际代码中去测试带来的优化。
详细介绍了每一个V8引擎的优化点在本文讨论范围以外,固然这里也有许多值得一提的技巧。记住这些技巧你就能减小那些性能低下的代码了。
function add(x, y) { return x+y; } add(1, 2); add('a','b'); add(my_custom_object, undefined);
更多内容能够去看Daniel Clifford在Google I/O的分享 Breaking the JavaScript Speed Limit with V8。 Optimizing For V8 — A Series也很是值得一读。
JavaScript中对象和数组之间只有一个的主要区别,那就是数组神奇的length属性。若是你本身来维护这个属性,那么V8中对象和数组的速度是同样快的。
对于应用程序开发人员,对象克隆是一个常见的问题。虽然各类基准测试能够证实V8对这个问题处理得很好,但仍要当心。复制大的东西一般是较慢的——不要这么作。JS中的for..in循环尤为糟糕,由于它有着恶魔般的规范,而且不管是在哪一个引擎中,均可能永远不会比任何对象快。
当你必定要在关键性能代码路径上复制对象时,使用数组或一个自定义的“拷贝构造函数”功能明确地复制每一个属性。这多是最快的方式:
function clone(original) { this.foo = original.foo; this.bar = original.bar; } var copy = new clone(original);
使用模块模式时缓存函数,可能会致使性能方面的提高。参阅下面的例子,由于它老是建立成员函数的新副本,你看到的变化可能会比较慢。
另外请注意,使用这种方法明显更优,不只仅是依靠原型模式(通过jsPerf测试确认)。
使用模块模式或原型模式时的性能提高
这是一个原型模式与模块模式的性能对比测试:
// 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