JS性能优化一些方案
摘自:http://www.china125.com/design/js/3631.htm
1. 避免全局查找
在一个函数中会用到全局对象存储为局部变量来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快些。
2. 定时器
如果针对的是不断运行的代码,不应该使用setTimeout,而应该是用setInterval,因为setTimeout每一次都会初始化一个定时器,而setInterval只会在开始的时候初始化一个定时器。
3. 字符串连接
如果要连接多个字符串,应该少使用+=,如:
x+=a; x+=b; x+=c; 应该写成 x+= a + b + c; 而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存,使用JavaScript数组来收集,最后使用join方法连接起来。
4. 避免with语句
和函数类似 ,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度,由于额外的作用域链的查找,在with语句中执行的代码肯定比外面执行的代码要慢,在能不使用with语句的时候尽量不要使用with语句。
5. 数字转换成字符串
一般最好用"" + 1来将数字转换成字符串,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:("" +) > String() > .toString() > new String()
6. 浮点数转换成整型
很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()。
7. 各种类型转换
如果定义了toString()方法来进行类型转换的话,推荐显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高
8. 多个类型声明
在JavaScript中所有变量都可以使用单个var语句来声明,这样就是组合在一起的语句,以减少整个脚本的执行时间,就如上面代码一样,上面代码格式也挺规范,让人一看就明了。
9. 插入迭代器
如var a=arr[i]; i++;前面两条语句可以写成var a=arr[i++]
10. 使用直接量
">
11. 使用DocumentFragment优化多次append
一旦需要更新DOM,请考虑使用文档碎片来构建DOM结构,然后再将其添加到现存的文档中。
12. 使用一次innerHTML赋值代替构建dom元素
对于大的DOM更改,使用innerHTML要比使用标准的DOM方法创建同样的DOM结构快得多。
13. 通过模板元素clone,替代createElement_x
很多人喜欢在则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点。
14. 使用firstChild和nextSibling代替childNodes遍历dom元素
15. 删除DOM节点
删除dom节点之前,一定要删除注册在该节点上的事件,不管是用observe方式还是用attachEvent方式注册的事件,否则将会产生无法回收的内存。另外,在removeChild和innerHTML=’’二者之间,尽量选择后者. 因为在sIEve(内存泄露监测工具)中监测的结果是用removeChild无法有效地释放dom节点。
16. 使用事件代理
任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理,使用这个知识就可以将事件处理程序附加到更高的地方负责多个目标的事件处理,同样,对于内容动态增加并且子节点都需要相同的事件处理函数的情况,可以把事件注册提到父节点上,这样就不需要为每个子节点注册事件监听了。另外,现有的js库都采用observe方式来创建事件监听,其实现上隔离了dom对象和事件处理函数之间的循环引用,所以应该尽量采用这种方式来创建事件监听。
17. 重复使用的调用结果,事先保存到局部变量
18. 注意NodeList
最小化访问NodeList的次数可以极大的改进脚本的性能。
编写的调用,获取了元素的childNodes属性,获取了元素的attributes属性,访问了特殊的集合,如document.forms、document.images等等,要了解了当使用NodeList对象时,合理使用会极大的提升代码执行速度。
19. 优化循环
可以使用下面几种方式来优化循环。
减值迭代
大多数循环使用一个从0开始、增加到某个特定值的迭代器,在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
简化终止条件
由于每次循环过程都会计算终止条件,所以必须保证它尽可能快,也就是说避免属性查找或者其它的操作,最好是将循环控制量保存到局部变量中,也就是说对数组或列表对象的遍历时,提前将length保存到局部变量中,避免在循环的每一步重复取值。
简化循环体
循环体是执行最多的,所以要确保其被最大限度的优化
使用后测试循环
在结构的问题,需要经常跳转回去。
最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。
20. 展开循环
当循环次数是确定的,消除循环并使用多次函数调用往往会更快。
21. 避免双重解释
尽量少使用eval函数
如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串,也就是尽量少使用eval函数, 使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间,而且使用Eval带来的安全性问题也是不容忽视的。
不要使用Function构造器
不要给setTimeout或者setInterval传递字符串参数
22. 缩短否定检测
23. 条件分支
将条件分支,按可能性顺序从高到低排列:可以减少解释器对条件的探测次数,在同一条件子的多(>2)条件分支时,使用switch优于if:switch分支选择的效率高于if,在IE下尤为明显。4分支的测试,IE下switch的执行时间约为if的一半,使用三目运算符替代条件分支。
24. 使用常量
重复值:任何在多处用到的值都应该抽取为一个常量,用户界面字符串:任何用于显示给用户的字符串,都应该抽取出来以方便国际化,URLs:在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL,任意可能会更改的值:每当你用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化,如果答案是“是”,那么这个值就应该被提取出来作为一个常量。
25. 避免与null进行比较
由于JavaScript是弱类型的,所以它不会做任何的自动类型检查,所以如果看到与null进行比较的代码,尝试使用以下技术替换,如果值应为一个引用类型,使用instanceof操作符检查其构造函数,如果值应为一个基本类型,作用typeof检查其类型,如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上。
26. 避免全局量
全局变量应该全部字母大写,各单词之间用_下划线来连接。尽可能避免全局变量和函数,尽量减少全局变量的使用,因为在一个页面中包含的所有JavaScript都在同一个域中运行。所以如果你的代码中声明了全局变量或者全局函数的话,后面的代码中载入的脚本文件中的同名变量和函数会覆盖掉(overwrite)你的。
27. 尊重对象的所有权
因为JavaScript可以在任何时候修改任意对象,这样就可以以不可预计的方式覆写默认的行为,所以如果你不负责维护某个对象,它的对象或者它的方法,那么你就不要对它进行修改,具体一点就是说:
-
不要为实例或原型添加属性
-
不要为实例或者原型添加方法
-
不要重定义已经存在的方法
-
不要重复定义其它团队成员已经实现的方法,永远不要修改不是由你所有的对象,你可以通过以下方式为对象创建新的功能。
-
创建包含所需功能的新对象,并用它与相关对象进行交互
-
创建自定义类型,继承需要进行修改的类型,然后可以为自定义类型添加额外功能
28. 循环引用
如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。内存泄露的后果是在浏览器关闭前,即使是刷新页面,这部分内存不会被浏览器释放。简单的循环引用:
但是通常不会出现这种情况。通常循环引用发生在为dom元素添加闭包作为expendo的时候。
init在执行的时候,当前上下文我们叫做context。这个时候,context引用了el,el引用了function,function引用了context。这时候形成了一个循环引用。
下面2种方法可以解决循环引用:
- 置空dom对象
将el置空,context中不包含对dom对象的引用,从而打断循环应用。如果我们需要将dom对象返回,可以用如下方法:
- 构造新的context
把function抽到新的context中,这样,function的context就不包含对el的引用,从而打断循环引用。
29. 通过javascript创建的dom对象,必须append到页面中
IE下,脚本创建的dom对象,如果没有append到页面中,刷新页面,这部分内存是不会回收的!
30. 释放dom元素占用的内存
将dom元素的innerHTML设置为空字符串,可以释放其子元素占用的内存。在rich应用中,用户也许会在一个页面上停留很长时间,可以使用该方法释放积累得越来越多的dom元素使用的内存。
31. 释放javascript对象
在rich应用中,随着实例化对象数量的增加,内存消耗会越来越大。所以应当及时释放对对象的引用,让GC能够回收这些内存控件。 对象:obj = null 对象属性:delete obj.myproperty 数组item:使用数组的splice方法释放数组中不用的item
32. 避免string的隐式装箱
对string的方法调用,比如'xxx'.length,浏览器会进行一个隐式的装箱操作,将字符串先转换成一个String对象。推荐对声明有可能使用String实例方法的字符串时,采用如下写法:
var myString = new String('Hello World');
33. 性能方面的注意事项
1. 尽量使用原生方法
2. switch语句相对if较快
通过将case语句按照最可能到最不可能的顺序进行组织
3. 位运算较快
当进行数字运算时,位运算操作要比任何布尔运算或者算数运算快
4. 巧用||和&&布尔运算符
34. 避免错误应注意的地方
每条语句末尾须加分号
在if语句中,即使条件表达式只有一条语句也要用{}把它括起来,以免后续如果添加了语句之后造成逻辑错误。
使用+号时需谨慎
JavaScript 和其他编程语言不同的是,在 JavaScript 中,'+'除了表示数字值相加,字符串相连接以外,还可以作一元运算符用,把字符串转换为数字。因而如果使用不当,则可能与自增符'++'混淆而引起计算错误。
使用return语句需要注意
一条有返回值的return语句不要用()括号来括住返回值,如果返回表达式,则表达式应与return关键字在同一行,以避免压缩时,压缩工具自动加分号而造成返回与开发人员不一致的结果。
34. ==和===的区别
避免在if和while语句的条件部分进行赋值,如if (a = b),应该写成if (a == b),但是在比较是否相等的情况下,最好使用全等运行符,也就是使用===和!==操作符会相对于==和!=会好点。==和!=操作符会进行类型强制转换。
35. 不要使用生偏语法
不要使用生偏语法,写让人迷惑的代码,虽然计算机能够正确识别并运行,但是晦涩难懂的代码不方便以后维护。
36. 函数返回统一类型
虽然JavaScript是弱类型的,对于函数来说,前面返回整数型数据,后面返回布尔值在编译和运行都可以正常通过,但为了规范和以后维护时容易理解,应保证函数应返回统一的数据类型。
37. 何时用单引号,何时用双引号
虽然在JavaScript当中,双引号和单引号都可以表示字符串, 为了避免混乱,我们建议在HTML中使用双引号,在JavaScript中使用单引号,但为了兼容各个浏览器,也为了解析时不会出错,定义JSON对象时,最好使用双引号。
38. 总是检查数据类型
要检查你的方法输入的所有数据,一方面是为了安全性,另一方面也是为了可用性。用户随时随地都会输入错误的数据。这不是因为他们蠢,而是因为他们很忙,并且思考的方式跟你不同。用typeof方法来检测你的function接受的输入是否合法
首先,由于JS是一种解释型语言,执行速度要比编译型语言慢得多。(注:,Chrome是第一款内置优化引擎,将JS编译成本地代码的浏览器,其它浏览器也陆续实现了JS的编译过程。但是,即使到了编译执行JS的新阶段,仍然会存在低效率的代码。)以下总结一些可以改进代码的整体性能的方法。
1.注意作用域
记住一点,随着作用域中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。所以,访问全局变量总是比访问局部变量要慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。
1). 避免全局查找(因为涉及作用域上的查找)
function updateUI() {
var imgs = document.getElementByTagName("img");
for(var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = document.title + " image " + i;
}
}
注意,updateUI中包含了二个对于全局变量document对象的引用,特别是循环中的document引用,查到次数是O(n),每次都要进行作用域链查找。通过创建一个指向document的局部变量,就可以通过限制一次全局查找来改进这个函数的性能。
function updateUI() {
var doc = document;
var imgs = doc.getElementByTagName("img");
for(var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = doc.title + " image " + i;
}
}
2). 避免with语句(with会创建自已的作用域,因此会增加其中执行代码的作用域的长度)
2.选择正确的方法
和其它语言一样,性能问题的一部分是和用于解决问题的算法或方法有关的,所以通过选择正确的方法也能起到优化作用。
1.避免不必要的属性查找
在JS中访问变量或数组都是O(1)操作,比访问对象上的属性更有效率,后者是一个O(n)操作。对象上的任何属性查找都要比访问变量或数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,即属性查找越多,执行时间越长。所以针对需要多次用到对象属性,应将其存储在局部变量。
2.优化循环
循环是编程中最常见的结构,优化循环是性能优化过程中很重要的一部分。一个循环的基本优化步骤如下:
减值迭代——大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。
简化终止条件——由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它O(n)的操作。
简化循环体——循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
使用后测试循环——最常用的for和while循环都是前测试循环,而如do-while循环可以避免最初终止条件的计算,因些计算更快。
for(var i = 0; i < values.length; i++) {
process(values[i]);
}
优化1:简化终止条件
for(var i = 0, len = values.length; i < len; i++) {
process(values[i]);
}
优化2:使用后测试循环(注意:使用后测试循环需要确保要处理的值至少有一个)
var i values.length - 1;
if(i > -1) {
do {
process(values[i]);
}while(--i >= 0);
}
3.展开循环
当循环的次数确定时,消除循环并使用多次函数调用往往更快
当循环的次数不确定时,可以使用Duff装置来优化。Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。如下:
// Jeff Greenberg for JS implementation of Duff's Device
// 假设:values.length > 0
function process(v) {
alert(v);
}
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17];
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
do {
switch(startAt) {
case 0 : process(values[i++]);
case 7 : process(values[i++]);
case 6 : process(values[i++]);
case 5 : process(values[i++]);
case 4 : process(values[i++]);
case 3 : process(values[i++]);
case 2 : process(values[i++]);
case 1 : process(values[i++]);
}
startAt = 0;
}while(--iterations > 0);
如上展开循环可以提升大数据集的处理速度。接下来给出更快的Duff装置技术,将do-while循环分成2个单独的循环。(注:这种方法几乎比原始的Duff装置实现快上40%。)
// Speed Up Your Site(New Riders, 2003)
function process(v) {
alert(v);
}
var values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17];
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if(leftover > 0) {
do {
process(values[i++]);
}while(--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
}while(--iterations > 0);
针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。
4.避免双重解释
当JS代码想解析JS代码时就会存在双重解释惩罚,当使用eval()函数或是Function构造函数以及使用setTimeout()传一个字符串时都会发生这种情况。如下
eval("alert('hello world');"); // 避免
var sayHi = new Function("alert('hello world');"); // 避免
setTimeout("alert('hello world');", 100);// 避免
以上代码是包含在字符串中的,即在JS代码运行的同时必须新启运一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,故这种代码要比直接解析要慢。以下这几个例子,除了极少情况下eval是必须的,应尽量避免使用上述。对于Function构造函数,直接写成一般的函数即可。对于setTimeout可以传入函数作为第一个参数。如下:
alert('hello world');
var sayHi = function() {
alert('hello world');
};
setTimeout(function() {
alert('hello world');
}, 100);
总之,若要提高代码性能,尽可能避免出现需要按照JS解释的代码。
5.性能的其它注意事项
原生方法更快——只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。
switch语句较快——若有一系列复杂的if-else语句,可以转换成单个switch语句则可以得到更快的代码,还可以通过将case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。
位运算较快——当进行数学运算时,位运算操作要比任何布尔运算或算数运算快。选择性地用位运算替换算数运算可以极大提升复杂计算的性能,诸如取模,逻辑与和逻辑或也可以考虑用位运算来替换。
3.最小化语句数
JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式
1.多个变量声明
// 避免
var i = 1;
var j = "hello";
var arr = [1,2,3];
var now = new Date();
// 提倡
var i = 1,
j = "hello",
arr = [1,2,3],
now = new Date();
2.插入迭代值
// 避免
var name = values[i];
i++;
// 提倡
var name = values[i++];
3.使用数组和对象字面量,避免使用构造函数Array(),Object()
// 避免
var a = new Array();
a[0] = 1;
a[1] = "hello";
a[2] = 45;
var o = new Obejct();
o.name = "bill";
o.age = 13;
// 提倡
var a = [1, "hello", 45];
var o = {
name : "bill",
age : 13
};
4.优化DOM交互
在JS中,DOM无疑是最慢的一部分,DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分,故理解如何优化与DOM的交互可以极大提高脚本完成的速度。
1.最小化现场更新
一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新,每一个更改,不管是插入单个字符还是移除整个片段,都有一个性能惩罚,因为浏览器需要重新计算无数尺寸以进行更新。现场更新进行的越多,代码完成执行所花的时间也越长。
2.多使用innerHTML
有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改,两者效率差不多,但对于大的DOM更改,innerHTML要比标准的DOM方法创建同样的DOM结构快得多。
当使用innerHTML设置为某个值时,后台会创建一个HTML解释器,然后使用内部的DOM调用来创建DOM结构,而非基于JS的DOM调用。由于内部方法是编译好的而非解释执行,故执行的更快。