javascript性能优化

        现在主流浏览器都在比拼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

核心语法优化

经过原型优化方法定义

1. 若是一个方法类型将被频繁构造,经过方法原型从外面定义附加方法,从而避免方法的重复定义。
2. 能够经过外 部原型的构造方式初始化值类型的变量定义。(这里强调值类型的缘由是,引用类型若是在原型中定义, 一个实例对引用类型的更改会影响到其余实例。)

这条规则中涉及到JavaScript中原型的概念,

  • 构造函数都有一个prototype属性,指向另外一个对象。这个对象的全部属性和方法,都会被构造函数的实例继承。咱们可 以把那些不变的属性和方法,直接定义在prototype对象上。

  • 能够经过对象实例访问保存在原型中的值,不能经过对象实例重写原型中的值。

  • 在实例中添加一个与实例原型同名属性,那该属性就会屏蔽原型中的属性。

  • 经过delete操做符能够删除实例中的属性。

例如如下代码以及相应的内存中原型表示以下,

1
2
3
4
5
6
7
8
9
10
11
function Person(){}
Person.prototype.name = "Nicholas" ;
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer" ;
Person.prototype.sayName = function (){
    alert( this .name);
};
var person1 = new Person();
person1.sayName(); //”Nicholas”
var person2 = new Person();
person2.sayName(); //”Nicholas”

image

原型附加方法测试能够参考http://jsperf.com/func-constructor

原型附加值类型变量测试能够参考http://jsperf.com/prototype2

避开闭包陷阱

1. 闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会致使内存泄漏。

2. 闭包的性能不如使用内部方法,更不如重用外部方法。

因为IE浏览器的DOM是用COM来实现的, COM的内存管理是经过引用计数的方式,引用计数有个难题就是循环引用,一旦DOM 引用了闭包(例如event handler),闭包的上层元素又引用了这个DOM,就会形成循环引用从而致使内存泄漏。

Figure 2 Circular References with Closures

关于Js内存泄漏能够参考

http://www.crockford.com/javascript/memory/leak.html

http://msdn.microsoft.com/en-us/library/bb250448%28v=vs.85%29.aspx

闭包与非闭包的测试http://jsperf.com/closure2

避免使用属性访问方法

1. JavaScript不须要属性访问方法,由于全部的属性都是外部可见的。
2. 添加属性访问方法只是增长了一层重定向 ,对于访问控制没有意义。

使用属性访问方法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Car() {    
  this .m_tireSize = 17;    
  this .m_maxSpeed = 250;
  this .GetTireSize = Car_get_tireSize;    
  this .SetTireSize = Car_put_tireSize;
}
function Car_get_tireSize() {    
  return this .m_tireSize;
}
function Car_put_tireSize(value) {    
  this .m_tireSize = value;
}
var ooCar = new Car();
var iTireSize = ooCar.GetTireSize();
ooCar.SetTireSize(iTireSize + 1);

直接访问属性示例

1
2
3
4
5
6
7
function Car() {    
  this .m_tireSize = 17;    
  this .m_maxSpeed = 250;
}
var perfCar = new Car();
var iTireSize = perfCar.m_tireSize;
perfCar.m_tireSize = iTireSize + 1;

使用属性访问与不使用属性访问的测试http://jsperf.com/property-accessor

避免在循环中使用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元素查询效率,样式表匹配效率下降,是页面性能最主要的瓶颈之一。

优化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

示例代码以下

<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>
// 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的调用自己就会损耗必定 性能。

setInterval(function() {
  animateFirst('');
}, 10);
setInterval(function() {
  animateSecond('');
}, 10);

使用同一个timer,

setInterval(function() {
  animateFirst('');
  animateSecond('');
}, 10);
相关文章
相关标签/搜索