Skip to content

[zz]优化JavaScript

2009/07/31
很tricky很valuable。
转载自:http://www.cnblogs.com/Mainz/archive/2009/03/19/1416775.html

下面是从《javascript高级编程》中摘录的:

下载时间

Web浏览器下载的是JavaScript源码,也就是所有的长变量与注释都会包含在内。这个因素和其他因素都会增加下载时间,这会增加脚本运行的总时间。增加下载时间的关键因素就是脚本所包含的字节数。

要记住一个关键数字是1160,这是能放入单个TCP-IP包中的字节数。最好能将每个JavaScript文件都保持在1160字节以下以获取最优的下载时间。

在JavaScript中,每个字符就是一个字节,因此,每个额外的字符(不管是变量名、函数名、或者注释)都会影响下载速度。部署JavaScript之前,都应该尽可能优化下载速度。

1.删除注释

2.删除制表符和空格

3.删除所有的换行

4.替换变量名

5.ECMAScript Cruncher
利用Thomas Loo开发的ECMAScript Cruncher(ESC 可以http://www.saltstorm.net/depo/esc/51AJAX.com下载)。ESC是一个小巧的Window Shell脚本。

利用Thomas Loo开发的ECMAScript Cruncher(ESC 可以下载)。ESC是一个小巧的Window Shell脚本。运行ESC,必须使用Windows系统。打开一个控制台窗口,输入以下命令:
cscript ESC.wsf -l [0-4] -ow outputfile.js inputfile.js [inputfile2.js]

第一部分,cscript是Windows
Shell脚本解释程序。文件名ESC.wsf是ESC的程序本身。然后是压缩等级,一个0到4的数值,表示要进行优化的等级。-ow选项表示下一个参数
是优化后输出的文件名。最后,剩下的参数是要进行优化的JavaScript文件。可以只给出一个要进行优化的文件,也可以有多个文件(多个文件估优化后
会按顺序放到输出文件中)。

ESC支持的四个优化等级如下:
0:不改变脚本,要将多个文件合到单个文件中时有用;
1:删除所有的注释;
2:除等级1外,再删除额外的制表符和空格;
3:除等级2外,再删除换行;
4:除等级3外,再进行变量名替换。

ESC擅长把变量名替换成无意义的名称。它不会更改构造名称、公用特性和公用方法名称。

使用ESC时要记住,如果某个JavaScript引用了另一个文件中的构造函数,4级优化会把对构造函数的引用替换成无意义的名称。解决方法是将两个文件合并成一个文件,这样就会保持构造函数的名称。

6.其他减少字节数的方法
1)替换布尔值
  考虑下面的例子

varbFound=false;
 
for(var i=0;i<aTest.length;i++){
 
if(aTest[i]==vTest) {bFound=true;}
 
}
 

可以替换为: 

varbFound=0;
for(var i=0;i if(aTest[i]==vTest) {bFound=1;}
 }

   2)缩短否定检测

if(oTest !=#ff0000) {
//do something
}
if(oTest !=null) {
//do something
}
if(oTest !=false) {
//do something
}

  虽然这些都正确,但用逻辑非操作符来操作也有同样的效果

if(!oTest) {
//do something
}

7.使用数组和对象字面量

varaTest = new Array;
var aTest =[];

第二行用了数组字面量,与第一行效果一样,但要短很多。

  类似,对象字面量也可用于节省空间,以下两行效果一行,但对象字面量要更简短

varaTest = new Object;
var aTest ={}; 

 如果要创建具有一些特性的一般对象,也可以使用字面量,如下:

varoFruit = new O;
oFruit.color="red";
oFruit.name="apple"; 

  前面的代码可用对象字面量来改写成这样:

varoFruit = {color:"red",name:"apple"};

一.执行效率

1. DOM

1.1 使用DocumentFragment优化多次append

说明:添加多个dom元素时,先将元素append到DocumentFragment中,最后统一将DocumentFragment添加到页面。
该做法可以减少页面渲染dom元素的次数。经IE和Fx下测试,在append1000个元素时,效率能提高10%-30%,Fx下提升较为明显。

服用前:

for (var i = 0; i < 1000; i++) {
    var el = document.createElement(‘p’);
    el.innerHTML = i;
    document.body.appendChild(el);
}

服用后:

var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
    var el = document.createElement(‘p’);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);

1.2 通过模板元素clone,替代createElement

说明:通过一个模板dom对象cloneNode,效率比直接创建element高。
性能提高不明显,约为10%左右。在低于100个元素create和append操作时,没有优势。

服用前:

var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
    var el = document.createElement(‘p’);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);   

服用后:

var frag = document.createDocumentFragment();
var pEl = document.getElementsByTagName(‘p’)[0];
for (var i = 0; i < 1000; i++) {
    var el = pEl.cloneNode(false);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);

1.3 使用一次innerHTML赋值代替构建dom元素

说明:根据数据构建列表样式的时候,使用设置列表容器innerHTML的方式,比构建dom元素并append到页面中的方式,效率有数量级上的提高。

服用前:

var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
    var el = document.createElement(‘p’);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);

服用后:

var html = [];
for (var i = 0; i < 1000; i++) {
    html.push(‘<p>’ + i + ‘</p>’);
}
document.body.innerHTML = html.join(”);

1.4 使用firstChild和nextSibling代替childNodes遍历dom元素

说明:约能获得30%-50%的性能提高。逆向遍历时使用lastChild和previousSibling。

服用前:

var nodes = element.childNodes;
for (var i = 0, l = nodes.length; i < l; i++) {
var node = nodes[i];
……
}

服用后:

var node = element.firstChild;
while (node) {
……
node = node.nextSibling;
}

2. 字符串

2.1 使用Array做为StringBuffer,代替字符串拼接的操作

说明:IE在对字符串拼接的时候,会创建临时的String对象;经测试,在IE下,当拼接的字符串越来越大时,运行效率会急剧下降。Fx和Opera都对字符串拼接操作进行了优化;经测试,在Fx下,使用Array的join方式执行时间约为直接字符串拼接的1.4倍。

服用前:

var now = new Date();
var str = ”;
for (var i = 0; i < 10000; i++) {
    str += ‘123456789123456789’;
}
alert(new Date() – now);

服用后:

var now = new Date();
var strBuffer = [];
for (var i = 0; i < 10000; i++) {
    strBuffer.push(‘123456789123456789’);
}
var str = strBuffer.join(”);
alert(new Date() – now);

3. 循环语句

3.1 将循环控制量保存到局部变量

说明:对数组和列表对象的遍历时,提前将length保存到局部变量中,避免在循环的每一步重复取值。

服用前:

var list = document.getElementsByTagName(‘p’);
for (var i = 0; i < list.length; i++) {
    ……
}

服用后:

var list = document.getElementsByTagName(‘p’);
for (var i = 0, l = list.length; i < l; i++) {
    ……
}

3.2 顺序无关的遍历时,用while替代for

说明:该方法可以减少局部变量的使用。比起效率优化,更能直接看到的是字符数量的优化。该做法有程序员强迫症的嫌疑。

服用前:

var arr = [1,2,3,4,5,6,7];
var sum = 0;
for (var i = 0, l = arr.length; i < l; i++) {
    sum += arr[i];
}   

服用后:

var arr = [1,2,3,4,5,6,7];
var sum = 0, l = arr.length;
while (l–) {
    sum += arr[l];
}

4. 条件分支

4.1 将条件分支,按可能性顺序从高到低排列

说明:可以减少解释器对条件的探测次数。

4.2 在同一条件子的多(>2)条件分支时,使用switch优于if

说明:switch分支选择的效率高于if,在IE下尤为明显。4分支的测试,IE下switch的执行时间约为if的一半。

4.3 使用三目运算符替代条件分支

服用前:

if (a > b) {
num = a;
} else {
num = b;
}

服用后:

num = a > b ? a : b;

5. 定时器

5.1 需要不断执行的时候,优先考虑使用setInterval

说明:setTimeout每一次都会初始化一个定时器。setInterval只会在开始的时候初始化一个定时器

服用前:

var timeoutTimes = 0;
function timeout () {
    timeoutTimes++;
    if (timeoutTimes < 10) {
        setTimeout(timeout, 10);
    }
}
timeout();

服用后:

var intervalTimes = 0;
function interval () {
    intervalTimes++;
    if (intervalTimes >= 10) {
        clearInterval(interv);
    }
}
var interv = setInterval(interval, 10);

5.2 使用function而不是string

说明:如果把字符串作为setTimeout和setInterval的参数,浏览器会先用这个字符串构建一个function。

服用前:

var num = 0;
setTimeout(‘num++’, 10);   

服用后:

var num = 0;
function addNum () {
    num++;
}
setTimeout(addNum, 10);

6. 其他

6.1 尽量不使用动态语法元素

说明:eval、Function、execScript等语句会再次使用javascript解析引擎进行解析,需要消耗大量的执行时间。

6.2 重复使用的调用结果,事先保存到局部变量

说明:避免多次取值的调用开销。

服用前:

var h1 = element1.clientHeight + num1;
var h2 = element1.clientHeight + num2;

服用后:

var eleHeight = element1.clientHeight;
var h1 = eleHeight + num1;
var h2 = eleHeight + num2;

6.3 使用直接量

说明:
var a = new Array(param,param,…) -> var a = []
var foo = new Object() -> var foo = {}
var reg = new RegExp() -> var reg = /…/

6.4 避免使用with

说明: with虽然可以缩短代码量,但是会在运行时构造一个新的scope。
OperaDev上还有这样的解释,使用with语句会使得解释器无法在语法解析阶段对代码进行优化。对此说法,无法验证。

服用前:

with (a.b.c.d) {
property1 = 1;
property2 = 2;
}

服用后:

var obj = a.b.c.d;
obj.property1 = 1;
obj.property2 = 2;

6.5 巧用||和&&布尔运算符

重要程度:★★★
服用前:

function eventHandler (e) {
if(!e) e = window.event;
}

服用后:

function eventHandler (e) {
e = e || window.event;
}

服用前:

if (myobj) {
doSomething(myobj);
}

服用后:

myobj && doSomething(myobj);

6.6 类型转换

说明:
1).    数字转换成字符串,应用"" + 1,性能上:("" +) > String() > .toString() > new String();
2).    浮点数转换成整型,不使用parseInt(), parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,建议使用Math.floor()或者Math.round()
3).    对于自定义的对象,推荐显式调用toString()。内部操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String。

二.内存管理

2.1 循环引用

说明:如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。内存泄露的后果是在浏览器关闭前,即使是刷新页面,这部分内存不会被浏览器释放。

简单的循环引用:

var el = document.getElementById(‘MyElement’);
var func = function () {…}
el.func = func;
func.element = el;

但是通常不会出现这种情况。通常循环引用发生在为dom元素添加闭包作为expendo的时候。
如:

function init() {

    var el = document.getElementById(‘MyElement’);
el.onclick = function () {……}
}
init();

init在执行的时候,当前上下文我们叫做context。这个时候,context引用了el,el引用了function,function引用了context。这时候形成了一个循环引用。
下面2种方法可以解决循环引用:

1)    置空dom对象

服用前:

function init() {
var el = document.getElementById(‘MyElement’);
el.onclick = function () {……}
}
init();

服用后:

function init() {
var el = document.getElementById(‘MyElement’);
el.onclick = function () {……}
el = null;
}

init();

将el置空,context中不包含对dom对象的引用,从而打断循环应用。
如果我们需要将dom对象返回,可以用如下方法:

服用前:

function init() {
    var el = document.getElementById(‘MyElement’);
    el.onclick = function () {……}
    return el;
}
init();

服用后:

function init() {
var el = document.getElementById(‘MyElement’);
el.onclick = function () {……}
try{
return el;
} finally {
    el = null;
}
}
init();

2)    构造新的context

服用前:

function init() {
    var el = document.getElementById(‘MyElement’);
    el.onclick = function () {……}
}
init();

服用后:

function elClickHandler() {……}
function init() {
    var el = document.getElementById(‘MyElement’);
    el.onclick = elClickHandler;
}
init();

把function抽到新的context中,这样,function的context就不包含对el的引用,从而打断循环引用。

2.2 通过javascript创建的dom对象,必须append到页面中

说明:IE下,脚本创建的dom对象,如果没有append到页面中,刷新页面,这部分内存是不会回收的!
示例代码:
    function create () {
        var gc = document.getElementById(‘GC’);
        for (var i = 0; i < 5000 ; i++)
        {
            var el = document.createElement(‘div’);
            el.innerHTML = "test";
            //下面这句可以注释掉,看看浏览器在任务管理器中,点击按钮然后刷新后的内存变化
            gc.appendChild(el);
        }
    }

2.3 释放dom元素占用的内存

说明:
将dom元素的innerHTML设置为空字符串,可以释放其子元素占用的内存。
在rich应用中,用户也许会在一个页面上停留很长时间,可以使用该方法释放积累得越来越多的dom元素使用的内存。

2.4 释放javascript对象

说明:在rich应用中,随着实例化对象数量的增加,内存消耗会越来越大。所以应当及时释放对对象的引用,让GC能够回收这些内存控件。
对象:obj = null
对象属性:delete obj.myproperty
数组item:使用数组的splice方法释放数组中不用的item

2.5 避免string的隐式装箱


说明:对string的方法调用,比如’xxx’.length,浏览器会进行一个隐式的装箱操作,将字符串先转换成一个String对象。推荐对声明有可能使用String实例方法的字符串时,采用如下写法:
var myString = new String(‘Hello World’);

 

语言相关:

循环

 

循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript中,我们可以使用for(;;),while(),for(in)
三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以,就应该尽量少用。for(;;)和while循
环,while循环的效率要优于for(;;),可能是因为for(;;)结构的问题,需要经常跳转回去。

 

局部变量和全局变量

局部变量的速度要比全局变量的访问速度更快,因为全局变量其实是全局对象的成员,而局部变量是放在函数的栈当中的。

 

不使用Eval

使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间。这时候使用JavaScript所支持的闭包可以实现函数模版(关于闭包的内容请参考函数式编程的有关内容)

 

减少对象查找

因为JavaScript的解释性,所以a.b.c.d.e,需要进行至少4次查询操作,先检查a再检查a中的b,再检查b中的c,如此往下。所以如果这样的表达式重复出现,只要可能,应该尽量少出现这样的表达式,可以利用局部变量,把它放入一个临时的地方进行查询。

这一点可以和循环结合起来,因为我们常常要根据字符串、数组的长度进行循环,而通常这个长度是不变的,比如每次查询a.length,就要额外进行一个操作,而预先把var len=a.length,则就少了一次查询。

 

字符串连接

如果是追加字符串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr。

如果要连接多个字符串,应该少使用+=,如

s+=a;

s+=b;

s+=c;

应该写成s+=a + b + c;

而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存。怎么用呢?使用JavaScript数组来收集,最后使用join方法连接起来,如下

var buf = new Array();

for(var i = 0; i < 100; i++){

buf.push(i.toString());

}

var all = buf.join("");

 

类型转换

类型转换是大家常犯的错误,因为JavaScript是动态类型语言,你不能指定变量的类型。

1. 把数字转换成字符串,应用"" + 1,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:

("" +) > String() > .toString() > new String()

这条其实和下面的“直接量”有点类似,尽量使用编译时就能使用的内部操作要比运行时使用的用户操作要快。

String()属于内部函数,所以速度很快,而.toString()要查询原型中的函数,所以速度逊色一些,new String()用于返回一个精确的副本。

2. 浮点数转换成整型,这个更容易出错,很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()。

另外,和第二节的对象查找中的问题不一样,Math是内部对象,所以Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。

3. 对于自定义的对象,如果定义了toString()方法来进行类型转换的话,推荐显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高

 

使用直接量

其实这个影响倒比较小,可以忽略。什么叫使用直接量,比如,JavaScript支持使用[param,param,param,…]来直接表
达一个数组,以往我们都使用new
Array(param,param,…),使用前者是引擎直接解释的,后者要调用一个Array内部构造器,所以要略微快一点点。

同样,var foo = {}的方式也比var foo = new Object();快,var reg = /../;要比var reg=new RegExp()快。

 

字符串遍历操作

对字符串进行循环操作,譬如替换、查找,应使用正则表达式,因为本身JavaScript的循环速度就比较慢,而正则表达式的操作是用C写成的语言的API,性能很好。

 

高级对象

自定义高级对象和Date、RegExp对象在构造时都会消耗大量时间。如果可以复用,应采用缓存的方式。

 

DOM相关:

插入HTML

很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低,如果需要直接插入HTML,可以找一个容器元素,比如指定一个div或者span,并设置他们的innerHTML来将自己的HTML代码插入到页面中。

 

对象查询

使用[“”]查询要比.items()更快,这和前面的减少对象查找的思路是一样的,调用.items()增加了一次查询和函数的调用。

 

创建DOM节点

通常我们可能会使用字符串直接写HTML来创建节点,其实这样做

  1. 无法保证代码的有效性

  2. 字符串操作效率低

所以应该是用document.createElement()方法,而如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为
使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多
元素,应该先准备一个样板节点。

 

定时器

如果针对的是不断运行的代码,不应该使用setTimeout,而应该是用setInterval。setTimeout每次要重新设置一个定时器。

 

其他:

脚本引擎

据我测试Microsoft的JScript的效率较Mozilla的Spidermonkey要差很多,无论是执行速度还是内存管理上,因为JScript现在基本也不更新了。但SpiderMonkey不能使用ActiveXObject

 

文件优化

文件优化也是一个很有效的手段,删除所有的空格和注释,把代码放入一行内,可以加快下载的速度,注意,是下载的速度而不是解析的速度,如果是本地,注释和空格并不会影响解释和执行速度。

 

批量增加Dom

尽量使用修改innerHTML的方式而不是用appendChild的方式; 因为使用innerHTML开销更小,速度更快,同时也更加内存安全.

有一点需要注意的是,用innerHTML方式添加时,一定不要在循环中使用 innerHTML += 的方式添加,这样反而会使速度减慢;
而是应该中间用array缓存起来,循环结束后调用 xx.innerHTML =
array.join(‘’);的方式,或者至少保存到string中再插到innerHTML中.

针对用户列表一块采用这种方式优化后,加载速度提升一倍.

 

单个增加Dom

这里是指要将新节点加载到一个内容不断变化的节点的情形,对于内容稳定的节点来说,随便怎么加都没有问题. 但是对于有动态内容的节点来说,为其添加子节点尽量使用 dom append的方式.

这是因为,dom append不会影响到其他的节点;而如果修改innerHTML属性的话,该父节点的所有子节点都会从dom树中剥离,再根据新的innerHTML值来重绘子节点dom树;所有注册到原来子节点的事件也会失效.

综上,如果在一个有动态内容的节点上 出现了 innerHTML += 的代码,就该考虑是否有问题了.

 

创建Dom节点

用createElement方式创建一个dom节点,有一个很重要的细节:
在执行完createElement代码之后,应该马上append到dom树中;
否则,如果在将这个孤立节点加载到dom树之前所做的赋值它的属性和innerHTML的操作都会引发该dom片段内存无法回收的问题.
这个不起眼细节,一旦遇到大量dom增删操作,就会引发内存的灾难.

 

删除Dom节点

删除dom节点之前,一定要删除注册在该节点上的事件,不管是用observe方式还是用attachEvent方式注册的事件,否则将会产生无法回收的内存.

另,在removeChild和innerHTML=’’二者之间,尽量选择后者. 因为在sIEve(内存泄露监测工具)中监测的结果是用removeChild无法有效地释放dom节点.

 

创建事件监听

现有的js库都采用observe方式来创建事件监听,其实现上隔离了dom对象和事件处理函数之间的循环引用,所以应该尽量采用这种方式来创建事件监听.

 

监听动态元素

Dom事件默认是向上冒泡的,发生在子节点中的事件,可以由父节点来处理. Event的 target/srcElement
仍是产生事件的最深层子节点.
这样,对于内容动态增加并且子节点都需要相同的事件处理函数的情况,可以把事件注册上提到父节点上,这样就不需要为每个子节点注册事件监听了.

同时,这样做也避免了产生无法回收的内存.即使是用Prototype的observe方式注册事件并在删除节点前调用stopObserving,也会产生出少量无法回收的内存,所以应该尽量少的为dom节点注册事件监听.

所以,当代码中出现在循环里注册事件时,也是我们该考虑事件上提机制的时候了.

 

HTML提纯

HTML提纯体现的是一种各负其责的思想. HTML只用来显示,尽量不出现和显示无关的属性.比如onclick事件,比如自定义的对象属性.

事件可以用前面的方法避免, 对象属性指的是这样的一种情景:
通常情况下,动态增加的内容都是有个对象和它对应,比如聊天室的用户列表,每个显示用户的dom节点都有一个user对象和它对应,这样在html中,应
该仅保留一个id属性和user对象对应,而其他的信息,则应通过user对象去获取.

 

另外注意:JavaScript在IE下的内存泄漏问题,详情见msdn:http://msdn.microsoft.com/en-us/library/bb250448.aspx

Advertisements
No comments yet

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s

%d 博主赞过: