
加载快速的网站就像是一辆快速的跑车,需要用到特别定制的零件. 图片来源: dHybridcars.
编写高性能代码时有一些常见的陷阱,在这篇文章中,我们将展示一些经过验证的、更好的编写代码方式。
如果你对JS引擎没有较深的了解,开发一个大型Web应用也没啥问题,就好比会开车的人也只是看过引擎盖而没有看过车盖内的引擎一样。鉴于Chrome是我的浏览器首选,所以谈一下它的JavaScript引擎。V8是由以下几个核心部分组成:

垃圾回收器尝试回收内存. 图片来源: 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; // nullo.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 elsevar b = foo();现在我们有一个引用指向bar对象,这样bar对象的生存周期就从foo的调用一直持续到调用者指定别的变量b(或b超出范围)。function sum (x) { function sumIt(y) {return x + y; }; return sumIt;}// Usagevar sumA = sum(4);var sumB = sumA(3);console.log(sumB); // Returns 7在sum调用上下文中生成的函数对象(sumIt)是无法被回收的,它被全局变量(sumA)所引用,并且可以通过sumA(n)调用。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已不存在外部引用了】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。这跟我们为了保持引用将闭包传给其他的函数是一样的。
做的过多还不如什么都不做. 图片来源: Tim Sheerman-Chase.
比如我们想要创建这样一个模块:
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"); });} };}();这段代码简单有效地完成了任务。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();事实证明,在这种情况下的带来的性能提升可以忽略不计。模板和原型的选择并没有真正提供更多的东西。也就是说,性能并不是开发者使用它们的原因,给代码带来的可读性、继承模型和可维护性才是真正的原因。function add(x, y) {return x+y;} add(1, 2); add("a","b"); add(my_custom_object, undefined);function clone(original) { this.foo = original.foo; this.bar = original.bar;}var copy = new clone(original);
使用模块模式或原型模式时的性能提升
这是一个原型模式与模块模式的性能对比测试:
// 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// 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 arrayfor(var i = 1; i <= 4; i++) {a.push(i);}
空字面量与预分配数组在不同的浏览器进行测试
Nitro (Safari)对预分配的数组更有利。而在其他引擎(V8,SpiderMonkey)中,预先分配并不是高效的。
预分配数组测试
// Empty arrayvar arr = [];for (var i = 0; i < 1000000; i++) { arr[i] = i;}// Pre-allocated arrayvar arr = new Array(1000000);for (var i = 0; i < 1000000; i++) { arr[i] = i;}
图片来源: Per Olof Forsberg.
理解和提高应用程序的性能是非常有用的同时,它也是困难的。我们推荐以下的步骤来解决性能的痛点:
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 timestotalTime = new Date - start;在这里,要测试的代码被放置在一个循环中,并运行一个设定的次数(例如6次)。在此之后,开始日期减去结束日期,就得出在循环中执行操作所花费的时间。
Chrome开发者工具的分析面板
分析过程开始获取代码性能基线,然后以时间线的形式体现。这将告诉我们代码需要多长时间运行。“Profiles”选项卡给了我们一个更好的视角来了解应用程序中发生了什么。JavaScript CPU分析文件展示了多少CPU时间被用于我们的代码,CSS选择器分析文件展示了多少时间花费在处理选择器上,堆快照显示多少内存正被用于我们的对象。
利用这些工具,我们可以分离、调整和重新分析来衡量我们的功能或操作性能优化是否真的起到了效果。
“Profile”选项卡展示了代码性能信息。
一个很好的分析介绍,阅读Zack Grossbart的 JavaScript Profiling With The Chrome Developer Tools。
提示:在理想情况下,若想确保你的分析并未受到已安装的应用程序或扩展的任何影响,可以使用--user-data-dir <empty_directory>标志来启动Chrome。在大多数情况下,这种方法优化测试应该是足够的,但也需要你更多的时间。这是V8标志能有所帮助的。

Chrome开发者工具中的内存统计
内存统计出我们团队所关心的私有内存使用、JavaScript堆的大小、DOM节点数量、存储清理、事件监听计数器和垃圾收集器正要回收的东西。推荐阅读Loreena Lee的“3快照”技术。该技术的要点是,在你的应用程序中记录一些行为,强制垃圾回收,检查DOM节点的数量有没有恢复到预期的基线,然后分析三个堆的快照来确定是否有内存泄漏。
与其了解事件与引用是如何工作的,不如遵循的标准规则来管理JavaScript中的内存。如果你想加载数据到的一个存满用户对象的Backbone集合中,你要清空这个集合使它不再占用内存,那必须这个集合的所有引用以及集合内对象的引用。一旦清楚了所用的引用,资源就会被回收。这就是标准的JavaScript垃圾回收规则。在文章中,Derick涵盖了许多使用Backbone.js时的常见内存缺陷,以及如何解决这些问题。

回流时间图表
你应该批量地触发回流或重绘,但是要节制地使用这些方法。尽量不处理DOM也很重要。可以使用DocumentFragment,一个轻量级的文档对象。你可以把它作为一种方法来提取文档树的一部分,或创建一个新的文档“片段”。与其不断地添加DOM节点,不如使用文档片段后只执行一次DOM插入操作,以避免过多的回流。
例如,我们写一个函数给一个元素添加20个div。如果只是简单地每次append一个div到元素中,这会触发20次回流。
function addDivs(element) { var div; for (var i = 0; i < 20; i ++) { div = document.createElement("div"); div.innerHTML = "Heya!"; element.appendChild(div); }}要解决这个问题,可以使用DocumentFragment来代替,我们可以每次添加一个新的div到里面。完成后将DocumentFragment添加到DOM中只会触发一次回流。
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);}可以参阅 Make the Web Faster,JavaScript Memory Optimization 和 Finding Memory Leaks。

一个JavaScript内存泄漏检测工具
有完整的文章介绍了如何使用这个工具,建议你自己到内存泄漏探测器项目页面看看。
如果你想知道为什么这样的工具还没集成到我们的开发工具,其原因有二。它最初是在Closure库中帮助我们捕捉一些特定的内存场景,它更适合作为一个外部工具。
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"Windows用户可以这样运行 chrome.exe –js-flags=”–trace-opt –trace-deopt”

高精度时间(HRT)提供了当前亚毫秒级的时间精度
目前HRT在Chrome(稳定版)中是以window.performance.webkitNow()方式使用,但在Chrome Canary中前缀被丢弃了,这使得它可以通过window.performance.now()方式调用。Paul Irish在HTML5Rocks上了关于HRT更多内容的文章。
现在我们知道当前的精准时间,那有可以准确测量页面性能的API吗?好吧,现在有个Navigation Timing API可以使用,这个API提供了一种简单的方式,来获取网页在加载呈现给用户时,精确和详细的时间测量记录。可以在console中使用window.performance.timing来获取时间信息:
显示在控制台中的时间信息
我们可以从上面的数据获取很多有用的信息,例如网络延时为responseEnd – fetchStart,页面加载时间为loadEventEnd – responseEnd,处理导航和页面加载的时间为loadEventEnd – navigationStart。
正如你所看到的,perfomance.memory的属性也能显示JavaScript的内存数据使用情况,如总的堆大小。
更多Navigation Timing API的细节,阅读 Sam Dutton的 Measuring Page Load Speed With Navigation Timing。

About:Tracing提供了浏览器的性能视图
这个工具的真正用处是允许你捕获Chrome的运行数据,这样你就可以适当地调整JavaScript执行,或优化资源加载。
Lilli Thompson有一篇写给游戏开发者的使用about:tracing分析WebGL游戏的文章,同时也适合JavaScript的开发者。
在Chrome的导航栏里可以输入about:memory,同样十分实用,可以获得每个tab页的内存使用情况,对定位内存泄漏很有帮助。

图片来源: Sally Hunter
谨记关注优化,但为了便利可以舍弃一些很小的优化。例如,有些开发者选择.forEach和Object.keys代替for和for..in循环,尽管这会更慢但使用更方便。要保证清醒的头脑,知道什么优化是需要的,什么优化是不需要的。
同时注意,虽然JavaScript引擎越来越快,但下一个真正的瓶颈是DOM。回流和重绘的减少也是重要的,所以必要时再去动DOM。还有就是要关注网络,HTTP请求是珍贵的,特别是移动终端上,因此要使用HTTP的缓存去减少资源的加载。
记住这几点可以保证你获取了本文的大部分信息,希望对你有所帮助!
原文:http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/
作者:Addy Osmani