JavaScript性能优化

如今主流浏览器都在比拼JavaScript引擎的执行速度,但最终都会达到一个理论极限,即无限接近编译后程序执行速度。 这种情况下决定程序速度的另一个重要因素就是代码本身。

在这里我们会分门别类的介绍JavaScript性能优化的技巧,并提供相应的测试用例,供大家在自己使用的浏览器上验证, 同时会对特定的JavaScript背景知识做一定的介绍。

目录

变量查找优化

变量声明带上var

1. 如果声明变量忘记了var,那么js引擎将会遍历整个作用域查找这个变量,结果不管找到与否,都是悲剧。

  • 如果在上级作用域找到了这个变量,上级作用域变量的内容将被无声的改写,导致莫名奇妙的错误发生。
  • 如果在上级作用域没有找到该变量,这个变量将自动被声明为全局变量,然而却都找不到这个全局变量的定义。

2. 基于上面逻辑,性能方面不带var声明变量自然要比带var速度慢

具体可以参考http://jsperf.com/withvar-withoutvar。下面是个简单的结果截图,蓝色为带var的情况,越长说明 速度越快。

image

慎用全局变量

1. 全局变量需要搜索更长的作用域链。

2. 全局变量的生命周期比局部变量长,不利于内存释放。

3. 过多的全局变量容易造成混淆,增大产生bug的可能性。

全局变量与局部变量的测试可以参考http://jsperf.com/local-global-var

 

以上两条还可以得出一条JavaScript常用的编程风格具有相同作用域变量通过一个var声明 。

这样方便查看该作用域所有的变量,JQuery源代码中就是用了这种风格。例如下面源代码

https://github.com/jquery/jquery/blob/master/src/core.js

1
2
3
4
jQuery.extend = jQuery.fn.extend = function () {
var options, name, src, copy, copyIsArray, clone,target = arguments[0] || {},i = 1,length =
 
arguments.length,deep = false ;

缓存重复使用的全局变量

1. 全局变量要比局部变量需要搜索的作用域长

2. 重复调用的方法也可以通过局部缓存来提速

3. 该项优化在IE上体现比较明显

缓存与不缓存变量的测试可以参考http://jsperf.com/localvarcache

JQuery源代码中也是用了类似的方法,https://github.com/jquery/jquery/blob/master/src/selector-native.js

1
2
3
4
5
6
7
8
9
10
var docElem = window.document.documentElement, selector_hasDuplicate,
matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector ||
 
docElem.msMatchesSelector,
selector_sortOrder = function ( a, b ) {
// Flag for duplicate removal
if ( a === b ) {
     selector_hasDuplicate = true ;
     return 0;
}

避免使用with

with语句将一个新的可变对象推入作用域链的头部,函数的所有局部变量现在处于第二个作用域链对象中,从而使局部变 量的访问代价提高。

1
2
3
4
5
6
7
8
9
10
11
var person = {
     name: “Nicholas",
     age: 30
}
function displayInfo() {
     var count = 5;
     with (person) {
         alert(name + ' is ' + age);
         alert( 'count is ' + count);
     }
}

以上代码的结果将name和age两个变量推入第一个作用域,如下图所示,

image

使用with与不使用with的测试可以参考http://jsperf.com/with-with

核心语法优化

避免在循环中使用try-catch

1. try-catch-finally语句在catch语句被执行的过程中会动态构造变量插入到当前域中,对性能有一定影响。 
2. 如 果需要异常处理机制,可以将其放在循环外层使用。

循环中使用try-catch

1
2
3
for ( var i = 0; i < 200; i++) {
  try {} catch (e) {}
}

循环外使用try-catch

1
2
3
try {
  for ( var i = 0; i < 200; i++) {}
} catch (e) {}

循环内与循环外使用try-catch的测试http://jsperf.com/try-catch

使用for代替for…in…遍历数组

for…in…内部实现是构造一个所有元素的列表,包括array继承的属性,然后再开始循环。相对for循环性能要慢。

StackOverflow上对这个for和for in的问题有个经典的回答,直接原文引用,

Q: I've been told not to use "for...in" with arrays in JavaScript. Why not?

A: The reason is that one construct...

1
2
3
4
5
6
<code> var a = [];
a[5] = 5; // Perfectly legal JavaScript that resizes the array.
 
for ( var i=0; i<a.length; i++) {
     // Iterates over numeric indexes from 0 to 5, as everyone expects.
}</code>

can sometimes be totally different from the other...

1
2
3
4
5
<code> var a = [];
a[5] = 5;
for ( var x in a) {
     // Shows only the explicitly set index of "5", and ignores 0-4
}</code>

Also consider that JavaScript libraries might do things like this, which will affect any array you create:

1
2
3
4
5
6
7
8
9
10
11
<code> // Somewhere deep in
 
your JavaScript library...
Array.prototype.foo = 1;
 
// Now you have no idea what the below code will do.
var a = [1,2,3,4,5];
for ( var x in a){
     // Now foo is a part of EVERY array and
     // will show up here as a value of 'x'.
}</code>

关于for和for…in…的测试可以看http://jsperf.com/forin

使用原始操作代替方法调用

方法调用一般封装了原始操作,在性能要求高的逻辑中,可以使用原始操作代替方法调用来提高性能。

原始操作

1
var min = a < b ? a : b;

方法实例

1
var min = Math.min(a, b);

关于方法调用和原始操作的测试参考http://jsperf.com/operator-function

传递方法取代方法字符串

一些方法例如setTimeout()/setInterval(),接受字符串或者方法实例作为参数。直接传递方法对象作为参数来避免对字 符串的二次解析。

传递方法

1
setTimeout(test, 1);

传递方法字符串

1
setTimeout( 'test()' , 1);

对应的测试可以参考http://jsperf.com/string-function

脚本装载优化

使用工具精简脚本

精简代码就是将代码中的空格和注释去除,也有更进一步的会对变量名称混淆+精简。

根据统计精简后文件大小平均减少21%,即使Gzip之后文件也会减少5%。

常用的工具如下,

例如Closure Compiler效果如下,

image

启用Gzip压缩

Gzip通常可以减少70%网页内容的大小,包括脚本、样式表、图片等文件。Gzip比deflate更高效,主流服务器都有相应的 压缩支持模块。

Gzip的工作流程为

  • 客户端在请求Accept-Encoding中声明可以支持gzip
  • 服务器将请求文档压缩,并在Content-Encoding中声明该回复为gzip格式
  • 客户端收到之后按照gzip解压缩

image

设置Cache-Control和Expires头

通过Cache-Control和Expires头可以将脚本文件缓存在客户端或者代理服务器上,可以减少脚本下载的时间。

1
2
3
4
5
6
7
8
9
Expires格式:
Expires = "Expires" ":" HTTP-date
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the
Expires field.
 
Cache-Control格式:
Cache-Control   = "Cache-Control" ":" 1#cache-directive
Cache-Control: public

具体的标准定义可以参考http1.1中的定义,简单来说Expires控制过期时间是多久,Cache-Control控制什么地方可以缓存 。

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9

异步加载脚本

脚本加载与解析会阻塞HTML渲染,可以通过异步加载方式来避免渲染阻塞。

异步加载的方式很多,比较通用的方法是通过类似下面的代码实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function loadjs
 
(script_filename){
     var script = document.createElement( 'script' );
     script.setAttribute( 'type' , 'text/javascript' );
     script.setAttribute( 'src' , script_filename);
     script.setAttribute( 'id' , 'script-id' );
 
     scriptElement = document.getElementById( 'script-id' );
     if (scriptElement){
         document.getElementsByTagName( 'head' )[0].removeChild(scriptElement);
     }
     document.getElementsByTagName( 'head' )[0].appendChild(script);
}
var script = 'scripts/alert.js' ;
loadjs(script);

DOM操作优化

DOM操作性能问题主要有以下原因,

  • DOM元素过多导致元素定位缓慢
  • 大量的DOM接口调用
  • DOM操作触发频繁的reflow(layout)和repaint

关于reflow(layout)和repaint可以参考下图,可以看到layout发生在repaint之前,所以layout相对来说会造成更多性能 损耗。

  • reflow(layout)就是计算页面元素的几何信息
  • repaint就是绘制页面元素

image

以下是一个wikipedia网站reflow的过程录像,

 

减少DOM元素数量

1. 在console中执行命令查看DOM元素数量

1
     document.getElementsByTagName( '*' ).length

2. Yahoo首页DOM元素数量在1200左右。正常页面大小一般不应该超过 1000。 
3. DOM元素过多会使DOM元素查询效率,样式表匹配效率降低,是页面性能最主要的瓶颈之一。


缓存DOM节点查找的结果

优化CSS样式转换

如果需要动态更改CSS样式,尽量采用触发reflow次数较少的方式。

例如以下代码逐条更改元素的几何属性,理论上会触发多次reflow

1
2
3
element.style.fontWeight = 'bold' ;
element.style.marginLeft= '30px' ;
element.style.marginRight = '30px' ;

可以通过直接设置元素的className直接设置,只会触发一次reflow

1
2
3
element.className =
 
'selectedAnchor' ;

具体的测试结果如下,

image

测试用例可以参考http://jsperf.com/css-class

优化节点添加

多个节点插入操作,即使在外面设置节点的元素和风格再插入,由于多个节点还是会引发多次reflow。优化的方法是创建 DocumentFragment,在其中插入节点后再添加到页面。

例如JQuery中所有的添加节点的操作如append,都是最终调用documentFragment来实现的,

http://code.jquery.com/jquery-1.10.2.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function
 
createSafeFragment( document ) {
     var list = nodeNames.split( "|" ),
         safeFrag = document.createDocumentFragment();
 
     if ( safeFrag.createElement ) {
         while ( list.length ) {
             safeFrag.createElement(
                 list.pop()
             );
         }
     }
     return safeFrag;
}

关于documentFragment对比直接添加节点的测试http://jsperf.com/fragment2

优化节点修改

对于节点的修改,可以考虑使用cloneNode在外部更新节点然后再通过replace与原始节点互换。

1
2
3
4
5
6
7
8
9
var orig = document.getElementById( 'container' );
var clone = orig.cloneNode( true );
var list = [ 'foo' , 'bar' , 'baz' ];
var contents;
for ( var i = 0; i < list.length; i++) {
   content = document.createTextNode(list[i]);
   clone.appendChild(content);
}
orig.parentNode.replaceChild(clone, orig);

对应的测试可以参考http://jsperf.com/clone-node2

减少使用元素位置操作

一般浏览器都会使用增量reflow的方式将需要reflow的操作积累到一定程度然后再一起触发,但是如果脚本中要获取以下 属性,那么积累的reflow将会马上执行,已得到准确的位置信息。

  • offsetLeft
  • offsetTop
  • offsetHeight
  • offsetWidth
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • getComputedStyle()

具体讨论可以参考这个链接http://www.stubbornella.org/content/2009/03/27/reflows-repaints-css- performance-making-your-javascript-slow/#comment-13157

避免遍历大量元素

避免对全局DOM元素进行遍历,如果parent已知可以指定parent在特定范围查询。

例如以下示例,

1
2
3
4
var elements = document.getElementsByTagName( '*' );
for (i = 0; i < elements.length; i++) {
   if (elements[i].hasAttribute( 'selected' )) {}
}

如果已知元素存在于一个较小的范围内,

1
2
3
4
5
6
var elements = document.getElementById
 
( 'canvas' ).getElementsByTagName( '*' );
for (i = 0; i < elements.length; i++) {
   if (elements[i].hasAttribute( 'selected' )) {}
}
1
 

相关测试可以参考http://jsperf.com/ranged-loop

事件优化

使用事件代理

1. 当存在多个元素需要注册事件时,在每个元素上绑定事件本身就会对性能有一定损耗。
2. 由于DOM Level2事件模 型中所有事件默认会传播到上层文档对象,可以借助这个机制在上层元素注册一个统一事件对不同子元素进行相应处理。

捕获型事件先发生。两种事件流会触发DOM中的所有对象,从document对象开始,也在document对象结束。

http://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html

image

示例代码如下

1
2
3
4
5
6
7
8
< ul id = "parent-list" >
     < li id = "post-1" >Item 1
     < li id = "post-2" >Item 2
     < li id = "post-3" >Item 3
     < li id = "post-4" >Item 4
     < li id = "post-5" >Item 5
     < li id = "post-6" >Item 6
</ li ></ ul >
1
2
3
4
5
6
7
8
9
// Get the element, add a click listener...
document.getElementById( "parent-list" ).addEventListener( "click" , function (e) {
     // e.target is the clicked element!
     // If it was a list item
     if (e.target && e.target.nodeName == "LI" ) {
         // List item found!  Output the ID!
         console.log( "List item " ,e.target.id.replace( "post-" ), " was clicked!" );
     }
});

对应的测试可以参考http://jsperf.com/event- delegate

动画优化

动画效果在缺少硬件加速支持的情况下反应缓慢,例如手机客户端

特效应该只在确实能改善用户体验时才使用,而不应用于炫耀或者弥补功能与可用性上的缺陷

至少要给用户一个选择可以禁用动画效果

设置动画元素为absolute或fixed

position: static 或position: relative元素应用动画效果会造成频繁的reflow

position: absolute或position: fixed 的元素应用动画效果只需要repaint

关于position的具体介绍可以参考

http://css- tricks.com/almanac/properties/p/position/

使用一个timer完成多个元素动画

setInterval和setTimeout是两个常用的实现动画的接口,用以间隔更新元素的风格与布局。

动画效果的帧率最优化的情况是使用一个timer完成多个对象的动画效果,其原因在于多个timer的调用本身就会损耗一定 性能。

1
2
3
4
5
6
setInterval( function () {
   animateFirst( '' );
}, 10);
setInterval( function () {
   animateSecond( '' );
}, 10);

使用同一个timer,

1
2
3
4
setInterval( function () {
   animateFirst( '' );
   animateSecond( '' );
}, 10);

 

以上是JavaScript性能提高的技巧总结,基本上都能够通过测试验证,但是限于篇幅没有把所有的测试结果都 贴出来。

最后再引用一句名人名言作为结尾,

Premature optimization is the root of all evil.                  -- Donald Knuth

 

 5.标明高度和宽度(如果浏览器没有找到这两个参数,它需要一边下载图片一边计算大小,如果图片很多,浏览器需要不断地调整页面。这不但影响速度,也影响浏览体验。
当浏览器知道了高度和宽度参数后,即使图片暂时无法显示,页面上也会腾出图片的空位,然后继续加载后面的内容。从而加载时间快了,浏览体验也更好了。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值