编写高性能JavaScript
英文链接:Writing Fast, Memory-Efficient JavaScript
非常多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用)。是专门为须要高速运行的大型JavaScript应用所设计的。假设你是一个开发人员。而且关心内存使用情况与页面性能。你应该了解用户浏览器中的JavaScript引擎是怎样运作的。
不管是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其它引擎,这样做能够帮助你更好地优化你的应用程序。
这并非说应该专门为某一浏览器或引擎做优化。千万别这么做。
可是,你应该问自己几个问题:
- 在我的代码里,能否够使代码更高效一些
- 主流的JavaScript引擎都做了哪些优化
- 什么是引擎无法优化的,垃圾回收器(GC)能否回收我所期望的东西
载入高速的站点就像是一辆高速的跑车。须要用到特别定制的零件. 图片来源: dHybridcars.
编写高性能代码时有一些常见的陷阱,在这篇文章中,我们将展示一些经过验证的、更好的编写代码方式。
那么,JavaScript在V8里是怎样工作的?
假设你对JS引擎没有较深的了解,开发一个大型Web应用也没啥问题。就好比会开车的人也仅仅是看过引擎盖而没有看过车盖内的引擎一样。
鉴于Chrome是我的浏览器首选,所以谈一下它的JavaScript引擎。
V8是由下面几个核心部分组成:
- 一个主要的编译器,它会在代码执行前解析JavaScript代码并生成本地机器码,而不是执行字节码或简单地解释它。这些代码最開始并非高度优化的。
- V8将对象构建为对象模型。在JavaScript中对象表现为关联数组,可是在V8中对象被看作是隐藏的类。一个为了优化查询的内部类型系统。
- 执行时分析器监视正在执行的系统,并标识了“hot”的函数(比如花费非常长时间执行的代码)。
- 优化编译器又一次编译和优化那些被执行时分析器标识为“hot”的代码,并进行“内联”等优化(比如用被调用者的主体替换函数调用的位置)。
- V8支持去优化。这意味着优化编译器如果发现对于代码优化的如果过于乐观。它会舍弃优化过的代码。
- V8有个垃圾收集器,了解它是怎样工作的和优化JavaScript一样重要。
垃圾回收
垃圾回收是内存管理的一种形式。事实上就是一个收集器的概念。尝试回收不再被使用的对象所占用的内存。在JavaScript这样的垃圾回收语言中。应用程序中仍在被引用的对象不会被清除。
手动消除对象引用在大多数情况下是没有必要的。通过简单地把变量放在须要它们的地方(理想情况下,尽可能是局部作用域。即它们被使用的函数里而不是函数外层)。一切将运作地非常好。
垃圾回收器尝试回收内存. 图片来源: Valtteri Mäki.
在JavaScript中,是不可能强制进行垃圾回收的。
你不应该这么做,由于垃圾收集过程是由执行时控制的,它知道什么是最好的清理时机。
“消除引用”的误解
网上有很多关于JavaScript内存回收的讨论都谈到delete这个keyword,尽管它能够被用来删除对象(map)中的属性(key)。但有部分开发人员觉得它能够用来强制“消除引用”。
建议尽可能避免使用delete。在以下的样例中delete o.x 的弊大于利,由于它改变了o的隐藏类。并使它成为一个"慢对象"。
var o = { x: 1 }; delete o.x; // true o.x; // undefined
你会非常easy地在流行的JS库中找到引用删除——这是具有语言目的性的。这里须要注意的是避免在执行时改动”hot”对象的结构。JavaScript引擎能够检測出这样的“hot”的对象。并尝试对其进行优化。假设对象在生命周期中其结构没有较大的改变,引擎将会更easy优化对象,而delete操作实际上会触发这样的较大的结构改变。因此不利于引擎的优化。
对于null是怎样工作也是有误解的。
将一个对象引用设置为null,并没有使对象变“空”。仅仅是将它的引用设置为空而已。
使用o.x= null比使用delete会更好些。但可能也不是非常必要。
var o = { x: 1 }; o = null; o; // null o.x // TypeError
假设此引用是当前对象的最后引用。那么该对象将被作为垃圾回收。
假设此引用不是当前对象的最后引用,则该对象是可訪问的且不会被垃圾回收。
另外须要注意的是。全局变量在页面的生命周期里是不被垃圾回收器清理的。
不管页面打开多久。JavaScript执行时全局对象作用域中的变量会一直存在。
var myGlobalNamespace = {};
全局对象仅仅会在刷新页面、导航到其它页面、关闭标签页或退出浏览器时才会被清理。函数作用域的变量将在超出作用域时被清理。即退出函数时,已经没有不论什么引用。这种变量就被清理了。
经验法则
为了使垃圾回收器尽早收集尽可能多的对象,不要hold着不再使用的对象。
这里有几件事须要记住:
- 正如前面提到的。在合适的范围内使用变量是手动消除引用的更好选择。即一个变量仅仅在一个函数作用域中使用。就不要在全局作用域声明它。这意味着更干净省心的代码。
- 确保解绑那些不再须要的事件监听器,尤其是那些即将被销毁的DOM对象所绑定的事件监听器。
- 假设使用的数据缓存在本地,确保清理一下缓存或使用老化机制,以避免大量不被重用的数据被存储。
函数
接下来,我们谈谈函数。
正如我们已经说过,垃圾收集的工作原理,是通过回收不再是訪问的内存块(对象)。为了更好地说明这一点,这里有一些样例。
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超出范围)。
闭包(CLOSURES)
当你看到一个函数,返回一个内部函数,该内部函数将获得范围外的訪问权,即使在外部函数运行之后。这是一个主要的闭包 —— 能够在特定的上下文中设置的变量的表达式。比如:
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.
比方我们想要创建这样一个模块:
- 须要一个本地的数据源包括数字ID
- 绘制包括这些数据的表格
- 加入事件处理程序,当用户点击的不论什么单元格时切换单元格的css class
这个问题有几个不同的因素,尽管也非常easy解决。
我们怎样存储数据,怎样高效地绘制表格而且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优化技巧
具体介绍了每一个V8引擎的优化点在本文讨论范围之外,当然这里也有很多值得一提的技巧。记住这些技巧你就能降低那些性能低下的代码了。
- 特定模式能够使V8摆脱优化的困境,比方说try-catch。
欲了解很多其它有关哪些函数能或不能进行优化,你能够在V8的脚本工具d8中使用–trace-opt file.js命令。
- 假设你关心速度。尽量使你的函数职责单一,即确保变量(包括属性,数组,函数參数)仅仅使用同样隐藏类包括的对象。举个样例,别这么干:
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也很值得一读。
对象VS数组:我应该用哪个?
- 假设你想存储一串数字,或者一些同样类型的对象,使用一个数组。
- 假设你语义上须要的是一堆的对象的属性(不同类型的),使用一个对象和属性。这在内存方面很高效,速度也相当快。
- 整数索引的元素,不管存储在一个数组或对象中,都要比遍历对象的属性快得多。
- 对象的属性比較复杂:它们能够被setter们创建。具有不同的枚举性和可写性。数组中则不具有如此的定制性,而仅仅存在有和无这两种状态。在引擎层面,这同意很多其它存储结构方面的优化。特别是当数组中存在数字时,比如当你须要容器时。不用定义具有x。y,z属性的类,而仅仅用数组就能够了。
JavaScript中对象和数组之间仅仅有一个的主要差别,那就是数组奇妙的length属性。假设你自己来维护这个属性。那么V8中对象和数组的速度是一样快的。
使用对象时的技巧
- 使用一个构造函数来创建对象。这将确保它创建的全部对象具有同样的隐藏类。并有助于避免更改这些类。
作为一个额外的优点,它也略快于Object.create()
- 你的应用中,对于使用不同类型的对象和其复杂度(在合理的范围内:长原型链往往是有害的。呈现仅仅有一个极少数属性的对象比大对象会快一点)是有没限制的。对于“hot”对象,尽量保持短原型链。而且少属性。
对象克隆
对于应用程序开发者,对象克隆是一个常见的问题。
尽管各种基准測试能够证明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.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数组的大小和类型。它通经常使用在体积不大的数组中。
// 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)的数据存在数组中绝不是一个好想法。比如var arr = [1, “1”, undefined, true, “true”]
正如我们所示结果。整数的数组是最快的。
稀疏数组与满数组
当你使用稀疏数组时,要注意訪问元素将远远慢于满数组。由于V8不会分配一整块空间给仅仅用到部分空间的数组。取而代之的是,它被管理在字典中,既节约了空间,但花费訪问的时间。
预分配空间VS动态分配
不要预分配大数组(如大于64K的元素)。其最大的大小,而应该动态分配。
在我们这篇文章的性能測试之前,请记住这仅仅适用部分JavaScript引擎。
空字面量与预分配数组在不同的浏览器进行測试
Nitro (Safari)对预分配的数组更有利。
而在其它引擎(V8,SpiderMonkey)中,预先分配并非高效的。
// 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应用的世界中。速度就是一切。没实用户希望用一个要花几秒钟计算某列总数或花几分钟汇总信息的表格应用。这是为什么你要在代码中压榨每一点性能的重要原因。
图片来源: Per Olof Forsberg.
理解和提高应用程序的性能是很实用的同一时候,它也是困难的。我们推荐下面的步骤来解决性能的痛点:
- 測量:在您的应用程序中找到慢的地方(约45%)
- 理解:找出实际的问题是什么(约45%)
- 修复它! (约10%)
以下推荐的一些工具和技术能够协助你。
基准化(BENCHMARKING)
有非常多方式来执行JavaScript代码片段的基准測试其性能——一般的如果是。基准简单地比較两个时间戳。这中模式被jsPerf团队指出,并在SunSpider和Kraken的基准套件中使用:
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;
在这里,要測试的代码被放置在一个循环中。并执行一个设定的次数(比如6次)。在此之后,開始日期减去结束日期,就得出在循环中执行操作所花费的时间。
然而。这种基准測试做的事情过于简单了,特别是假设你想执行在多个浏览器和环境的基准。垃圾收集器本身对结果是有一定影响的。即使你使用window.performance这种解决方式。也必须考虑到这些缺陷。
无论你是否仅仅执行基准部分的代码,编写一个測试套件或编码基准库,JavaScript基准事实上比你想象的很多其它。如需更具体的指南基准。我强烈建议你阅读由Mathias Bynens和John-David Dalton提供的Javascript基准測试。
分析(PROFILING)
Chrome开发人员工具为JavaScript分析有非常好的支持。能够使用此功能检測哪些函数占用了大部分时间。这样你就能够去优化它们。
这非常重要。即使是代码非常小的改变会对总体表现产生重要的影响。
Chrome开发人员工具的分析面板
分析过程開始获代替码性能基线。然后以时间线的形式体现。这将告诉我们代码须要多长时间执行。
“Profiles”选项卡给了我们一个更好的视角来了解应用程序中发生了什么。JavaScript CPU分析文件展示了多少CPU时间被用于我们的代码,CSS选择器分析文件展示了多少时间花费在处理选择器上。堆快照显示多少内存正被用于我们的对象。
利用这些工具。我们能够分离、调整和又一次分析来衡量我们的功能或操作性能优化是否真的起到了效果。
“Profile”选项卡展示了代码性能信息。
一个非常好的分析介绍。阅读Zack Grossbart的 JavaScript Profiling With The Chrome Developer Tools。
提示:在理想情况下,若想确保你的分析并未受到已安装的应用程序或扩展的不论什么影响,能够使用--user-data-dir <empty_directory>
标志来启动Chrome。在大多数情况下,这样的方法优化測试应该是足够的,但也须要你很多其它的时间。这是V8标志能有所帮助的。
避免内存泄漏——3快照技术
在谷歌内部,Chrome开发人员工具被Gmail等团队大量使用,用来帮助发现和排除内存泄漏。
Chrome开发人员工具中的内存统计
内存统计出我们团队所关心的私有内存使用、JavaScript堆的大小、DOM节点数量、存储清理、事件监听计数器和垃圾收集器正要回收的东西。推荐阅读Loreena Lee的“3快照”技术。该技术的要点是,在你的应用程序中记录一些行为,强制垃圾回收。检查DOM节点的数量有没有恢复到预期的基线。然后分析三个堆的快照来确定是否有内存泄漏。
单页面应用的内存管理
单页面应用程序(比如AngularJS。Backbone,Ember)的内存管理是很重要的,它们差点儿永远不会刷新页面。
这意味着内存泄漏可能相当明显。移动终端上的单页面应用充满了陷阱,由于设备的内存有限。并在长期执行Emailclient或社交网络等应用程序。
能力愈大责任愈重。
有非常多办法解决问题。
在Backbone中。确保使用dispose()来处理旧视图和引用(眼下在Backbone(Edge)中可用)。这个函数是近期加上的,移除加入到视图“event”对象中的处理函数,以及通过传给view的第三个參数(回调上下文)的model或collection的事件监听器。dispose()也会被视图的remove()调用,处理当元素被移除时的主要清理工作。Ember 等其它的库当检測到元素被移除时,会清理监听器以避免内存泄漏。
Derick Bailey的一些明智的建议:
与其了解事件与引用是怎样工作的,不如遵循的标准规则来管理JavaScript中的内存。
假设你想载入数据到的一个存满用户对象的Backbone集合中。你要清空这个集合使它不再占用内存。那必须这个集合的全部引用以及集合内对象的引用。一旦清楚了所用的引用,资源就会被回收。这就是标准的JavaScript垃圾回收规则。
在文章中,Derick涵盖了很多使用Backbone.js时的常见内存缺陷,以及怎样解决这些问题。
Felix Geisendörfer的在Node中调试内存泄漏的教程也值得一读,尤其是当它形成了更广泛SPA堆栈的一部分。
降低回流(REFLOWS)
当浏览器又一次渲染文档中的元素时须要 又一次计算它们的位置和几何形状,我们称之为回流。回流会堵塞用户在浏览器中的操作,因此理解提升回流时间是很有帮助的。
回流时间图表
你应该批量地触发回流或重绘,可是要克制地使用这些方法。
尽量不处理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。
JS内存泄漏探測器
为了帮助发现JavaScript内存泄漏。谷歌的开发者((Marja Hölttä和Jochen Eisinger)开发了一种工具,它与Chrome开发者工具结合使用。检索堆的快照并检測出是什么对象导致了内存泄漏。
一个JavaScript内存泄漏检測工具
有完整的文章介绍了怎样使用这个工具,建议你自己到内存泄漏探測器项目页面看看。
假设你想知道为什么这种工具还没集成到我们的开发工具,其原因有二。
它最初是在Closure库中帮助我们捕捉一些特定的内存场景。它更适合作为一个外部工具。
V8优化调试和垃圾回收的标志位
Chrome支持直接通过传递一些标志给V8。以获得更具体的引擎优化输出结果。比如,这样能够追踪V8的优化:
"/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的处理脚本用*(星号)标识优化过的函数,用~(波浪号)表示未优化的函数。
假设你有兴趣了解很多其它关于V8的标志和V8的内部是怎样工作的,强烈建议 阅读Vyacheslav Egorov的excellent post on V8 internals。
HIGH-RESOLUTION TIME 和 NAVIGATION TIMING API
高精度时间(HRT)是一个提供不受系统时间和用户调整影响的亚毫秒级高精度时间接口,能够把它当做是比 new Date 和 Date.now()更精准的度量方法。
这对我们编写基准測试帮助非常大。
高精度时间(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:MEMORY 和 ABOUT:TRACING
Chrome中的about:tracing提供了浏览器的性能视图,记录了Chrome的全部线程、tab页和进程。
About:Tracing提供了浏览器的性能视图
这个工具的真正用处是同意你捕获Chrome的执行数据。这样你就能够适当地调整JavaScript执行,或优化资源载入。
Lilli Thompson有一篇写给游戏开发人员的使用about:tracing分析WebGL游戏的文章,同一时候也适合JavaScript的开发人员。
在Chrome的导航栏里能够输入about:memory,相同十分有用,能够获得每一个tab页的内存使用情况。对定位内存泄漏非常有帮助。
总结
我们看到,JavaScript的世界中有非常多隐藏的陷阱,且并没有提升性能的银弹。
仅仅有把一些优化方案综合使用到(现实世界)測试环境,才干获得最大的性能收益。即便如此,了解引擎是怎样解释和优化代码,能够帮助你调整应用程序。
測量,理解,修复。不断反复这个过程。
图片来源: Sally Hunter
谨记关注优化,但为了便利能够舍弃一些非常小的优化。比如,有些开发人员选择.forEach和Object.keys取代for和for..in循环。虽然这会更慢但使用更方便。要保证清醒的头脑。知道什么优化是须要的。什么优化是不须要的。
同一时候注意,尽管JavaScript引擎越来越快。但下一个真正的瓶颈是DOM。回流和重绘的降低也是重要的。所以必要时再去动DOM。还有就是要关注网络,HTTP请求是珍贵的。特别是移动终端上。因此要使用HTTP的缓存去降低资源的载入。
记住这几点能够保证你获取了本文的大部分信息。希望对你有所帮助!
posted on 2017-06-16 19:43 yjbjingcha 阅读(2173) 评论(0) 编辑 收藏 举报