高性能的JavaScript
1、加快JavaScript文件的加载速度
默认情况下,浏览器在解析页面时遇到JavaScript引用就会停止后续的HTML代码解析,直到等待JavaScript代码文件下载和运行完成后才继续解析剩余的HTML代码。推荐的做法是尽量将JavaScript代码的引用放置在<body>的底部。
相比把脚本引用放置于页面底部的方式或动态创建script元素,给<script>设置defer和async属性的方式更优雅,它在兼顾代码可读性的同时也提高了代码加载执行的性能。defer就是在告诉浏览器JavaScript代码不会产生任何的页面内容,因此浏览器可以在加载此引用时继续解析页面后续的内容。async属性则表明可以以异步的方式加载和执行(不受限于顺序)JavaScript代码。
还可以按需加载JavaScript文件。按需加载的方式能更有效地减少初始加载的JavaScript代码量。推荐使用成熟的JavaScript加载框架,例如HeadJS、RequireJS、LABjs等JavaScript模块加载框架。
2、养成良好的编码习惯,提高代码的运行速度
在编写JavaScript代码时,首先是保证代码的可读性和可维护,在此前提下才是选用高性能的编码方式。
嵌套循环时把大循环作为内循环、尽量避免循环内定义变量、在条件分支中创建只有在分支中才需要用到的对象、使用直接量代替对象、缓存计算结果减少重复计算等。
1)少用for-in循环
for-in循环提供了一种遍历对象属性的能力,但它的性能很差,应尽量使用for循环代替。
2)谨慎使用eval
3)正确使用数组
在索引时,一个混合了多种类型的数组将比类型单一的数组慢很多。因此,应使用数组保存类型单一的数据,而在其他情况下使用对象。
4)正确地内存回收
3、使用高性能的变量或属性值读取方式
作用域就是变量或函数的作用范围,JavaScript中最大的作用域就是全局作用域。最小的作用域是函数。
多个函数嵌套定义时,就会形成作用域包含的关系,这个关系成为作用域链。若一个函数在运行中遇到一个变量,就会从最近的作用域开始进行搜索,如果找到了就会使用这个变量,如果没有找到则会进入外层的作用域中查找,如此反复,直到找到此变量的定义,或者未找到而判定变量未定义为止。
一个变量在作用域链上查找的层级越多则读取的速度越慢,因此,函数中局部变量的访问是最快的,全局变量的访问是最慢的。为了提高变量的读取性能,最佳的实践是尽量减少变量访问时在作用域链上查找的层级,最好将变量定义为本作用域的局部变量,尽量不要定义全局变量。如果频繁地访问一个外作用域的变量,最好是用一个局部变量保存外部变量
另外一个和作用域相关的性能损耗是with语句的使用。with表达式的使用也会形成一个作用域,无形中改变了上下文作用域链的深度。try-catch表达式的catch块中也会产生一个作用域。因此,最好将catch块中的处理交给一个函数,避免了在内部访问外部域的变量。
对象的属性读取也存在和变量读取时类似的性能影响,读取对象上的属性值时会搜索对象的原型链。
在JavaScript中,对象的构造函数中有一个名为prototype的对象,即为原型对象,这个对象上的属性和方法是共享给所有实例对象的。所以说,实例对象上的属性和方法来自于两个地方:自身和对应的原型对象。因为原型对象本身也可以是其他构造函数的实例对象,所以原型对象中的属性和方法也可能来自于其作为作为实例对象时对应的原型对象上。这就形成了一个由各原型对象组成的链条,成为原型链。原型链的顶端是构造函数Object中名为prototype的对象。
查找对象上的属性和方法,首先会查找自身是否存在此属性或方法,如果未找到,则会继续在原型链上查找,直到找到或未找到返回undefined值为止。
function Person (name){
this.name=name;
}
Person.prototype= {location: 'china'};
var personA = new Person('name1');
var personB = new Person('name2');
alter (personA.location); //china
alter (personB.location); //china
在原型链上检索的层级越多,性能越差,即使是读取在对象上直接定义的属性也比读取局部变量慢。因此,如果在代码中要频繁取得某个对象的属性值,尤其是此属性来自于对象上的原型对象上时,最佳的做法是把属性值缓存在局部变量中,提高读取对象属性的性能。
for (var i = 0;i < numbers.length; i++){
numbers[i] *= 2;
}
//性能改进的方案
for (var i = 0,len = numbers.length; i < len; i++){
numbers[i] *= 2;
}
4、高效地DOM操作
文档对象模型(DOM)是一个独立于特定语言的应用程序接口。尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面性能优化的一个主要的关注点就是DOM操作的优化。DOM操作优化的总体原则是尽量减少DOM操作。
在浏览器中,DOM的实现和ECMAScript的实现是分离的。通过JavaScript代码调用DOM接口,相当与两个独立模块的交互。DOM操作对性能影响最大其实是因为它导致了浏览器的重绘(repaint)和重排(reflow)。
浏览器的渲染原理:从下载文档到渲染页面的过程中,浏览器会通过解析HTML文档来构建DOM树,解析CSS产生CSS规则树。JavaScript代码在解析过程中,可能会修改生成的DOM树和CSS规则树。之后根据DOM树和CSS规则树构建渲染树,在这个过程中,CSS会根据选择器匹配HTML元素。渲染树包括了每个元素的大小、边距等样式属性,渲染树中不包括隐藏元素即head元素等不可见元素。最后浏览器根据元素的坐标和大小来计算每个元素的位置,并绘制这些元素到页面上。
重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没有改变;重排则是元素的位置或尺寸发生了改变,浏览器需要重新计算渲染树,导致渲染树的一部分或全部发生变化。重排的代价比重绘的代价高很多,重绘会影响部分的元素,而重排则有可能影响全部的元素。
如下的操作会导致重绘或重排:
1)增加、删除和修改可见DOM元素。
2)页面初始化的渲染。
3)移动DOM元素。
4)修改CSS样式,改变DOM元素的尺寸。
5)DOM元素内容改变,使得尺寸被撑大。
6)浏览器窗口尺寸改变。
7)浏览器窗口滚动。
在进行DOM操作时,遵循以下的最佳实践来降低由于重排或重绘带来的影响:
1)合并多次的DOM操作为单次的DOM操作
element.style.borderColor = '#f00';
element.style.borderStyle = 'solid';
element.style.borderWidth = '1px';
//优化方案1
element.style.cssText += 'border: 1px solid #f00;';
//优化方案2
element.className += 'empty';
//HTML5:element.classList.add('empty');
2)把DOM元素离线或隐藏后修改
对脱离了页面布局流的DOM元素操作就不会导致页面的性能问题。
大批量修改DOM元素,具体的方式主要有以下3种:
(1)使用文档片段
文档片段是一个轻量级的document对象,并不会和特定的页面关联。
var fragment = docment.createDocmentFragment ();
//大量基于fragment的DOM操作
...
docment.getElementById('myElement').appendChild(fragment);
(2)通过设置DOM元素的display样式为none来隐藏元素
var myElement = document.getElementById('myElement');
myElenment.style.display = 'none';
//一些基于myElement的DOM操作
...
myElement.style.display = 'block';
(3)克隆DOM元素到内存中
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
//一些基于clone的DOM操作
...
old.parentNode.replaceChild(clone, old);
3)设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面中具有动画效果的DOM元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面的重排,只涉及动画元素自身的重排了。
4)谨慎取得DOM元素的布局信息
获取DOM元素的布局信息会有性能的损耗,如果存在重复调用。最佳的做法是尽量把这些值缓存在局部变量中。
for (var i=0; i<len; i++) {
myElement[i].style.top = targetElement.offsetTop + i*5 +'px';
}
//优化方案
var targetTop = targetElement.offsetTop;
for (var i=0; i<len; i++) {
myElement[i].style.top = targetTop + i*5 +'px';
}
另外,因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或重排,所以在有大批量DOM操作时,应避免获取DOM元素的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好在DOM操作之前就取得。
var newWidth = div1.offsetWidth + 10;
div1.style.width = newWidth + 'px';
var newHeight = div1.offsetHeight + 10; //强制页面重排
div1.style.height = newHeight + 'px'; //又会重排一次
//优化方案
var newWidth = div1.offsetWidth + 10;
var newHeight = div1.offsetHeight + 10;
div1.style.width = newWidth + 'px';
div1.style.height = newHeight + 'px';
5)使用事件托管方式绑定事件
在DOM事件上绑定事件会影响页面的性能,一方面,绑定事件本身会占用处理时间,另一方面,浏览器保存事件绑定,绑定事件也会占用内存。在页面中绑定的事件越少越好。一个优雅的手段是使用事件托管方式,即利用事件冒泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不同的源元素做不同的处理。
5、使用辅助工具优化JavaScript代码性能
性能优化一定要有目标和尺度,而不是靠感觉。
在做JavaScript代码性能优化时,首先是查看JavaScript代码文件的加载情况。可以使用各浏览器自带的开发工具。
很多性能检查工具也会检查代码文件的加载情况,并给出有价值的建议,如压缩代码、合并代码、调整代码加载顺序、延迟加载代码等。这类工具有PageSpeed、YSlow等。这类工具会从页面整体进行检查,而不是仅仅检查JavaScript代码文件。
高性能的Web网页,要求网页渲染过程中达到理想的60帧/秒的帧率。