最初的 JavaScript 是一种解释型的语言,所以在执行速度上比编译型语言慢得多。后面有了 Chrome,它内置了优化引擎,把 JavaScript 编译为本地代码再执行,很多浏览器纷纷效仿,所以现在的 JavaScript 已经是编译型的语言咯O(∩_∩)O~
1 注意作用域
1.1 for 循环中的全局变量
使用全局变量或函数的查找开销比局部变量或函数大得多,因为这会涉及到作用域链上的查找:
//避免
function updateUI() {
var imgs = document.getElementsByTagName("img");
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
这个函数包含了 3 个对全局对象 document 的引用,所以如果 for 循环执行了几百次,那么每次循环都会进行一次全局查找!可以创建一个指向 document 对象的局部变量,就可以改善这个函数的执行性能:
//推荐
function updateUI() {
var doc = document;
var imgs = doc.getElementsByTagName("img");
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
把一个函数中可能会用到的多次全局对象存储为局部变量是一个很好的实践。
1.2 避免 with 语句
性能非常重要的地方一定要避免使用 with 语句!因为 with 语句会创建自己的作用域,而这增加了作用域链的长度,所以查找起来要比正常的代码来得慢!
使用 with 语句,无非是为了消除额外的字符,而这可以通过使用局部变量来实现。
//避免
function updateBody() {
with (document.body) {
console.log(tagName);
innerHTML = "Hello world!";
}
}
建议使用局部变量:
//推荐
function updateBody() {
var body = document.body;
console.log(body.tagName);
body.innerHTML = "Hello world!";
}
2 选择正确的方法
2.1 避免不必要的属性查找
我们一般是用 O 符号来表示算法的复杂度。最简单、最快的算法是常数值:O(1)。下面列出 JavaScript 中最常见的算法类型:
访问数组元素也是一个 O(1) 操作:
//访问数组元素,O(1)
var values = [5, 10];
var sum = value[0] + value[1];
console.log(sum);
而访问对象上的属性则是一个 O(n) 的操作,因为还必须在原型链中进行搜索,所以如果查找的属性越多,执行的时间就会越长:
//访问对象上的属性,O(n)
var values = {first: 5, second: 10};
var sum = values.first + values.second;
console.log(sum);
还要注意为了获取单个值的多重属性查找:
//获取单个值的多重查找(6 次)
var query = window.location.href.substring(window.location.href.indexOf("?"));
如果出现多次用到某个对象的属性,就应该把它存在局部变量中。这样虽然第一次访问会是 O(n),但后续的访问都会是 O(1),性能就会得到有效的改善:
//重写(4 次)
var url = window.location.href;
var query = url.substring(url.indexOf("?"));
在大脚本中进行这种优化,就能获得更多的好处O(∩_∩)O~
尽可能利用局部变量把属性查找转换为值查找。还有,如果可以使用索引位置对数组进行访问的话,就尽可能这样做啦O(∩_∩)O~
2.2 优化循环
基本优化步骤如下:
- 减值迭代——很多情况下,从最大值开始,在循环中不断减值的迭代器会更加高效。
- 简化终止条件——因为没有循环都会计算终止条件,所以要保证它尽可能地快!
- 简化循环体——执行最多的代码就是循环体,所以要对它进行最大限度的优化。那些没必要出现在循环体中的密集计算都要移出去哦O(∩_∩)O~
- 使用后测试循环——do/while,它可以避免最初对终止条件的计算,所以运行的更快。
举一个例子:
function process() {
}
//基本 for 循环
for (var i = 0; i < values.length; i++) {
process(values[i]);
}
假设值的处理顺序不重要,那么我们可以把上面的代码改为减值迭代:
//改为 i 减值
for (var i = values.length - 1; i >= 0; i--) {
process(values[i]);
}
减值迭代之所以更优是因为:它把终止条件从 values.length 的 O(n) 调用简化为了 0 的 O(1) 调用!也可以把 values.length 存为局部变量:
//存为局部变量
for (var i = 0, len = values.length; i < len; i++) {
process(values[i]);
}
还可以改为后测试循环:
//再改为后测试循环
var i = values.length - 1;
if (i > -1) {
do {
process(values[i]);
} while (--i >= 0);
}
推荐存为局部变量的方法,因为这样代码看起来会很简洁O(∩_∩)O~
2.3 展开循环
当循环的次数是确定的,那么消除循环并使用多次循环调用,往往会运行的更快。
如果循环的次数是不确定的,那么可以使用 Duff 装置技术。它是通过计算迭代的次数是否为 8 的倍数,然后把一个循环展开为一系列的语句:
//Duff 装置
//假设 values.length > 0
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);
假设数组中有 10 个值,那么 startAt 等于 2,那么在最开始时,process() 会被调用两次(因为 case 都没有 break,所以它们都穿越咯O(∩_∩)O~),接着 startAt 被重置为 0,所以在下一个循环中,,process() 会被调用八次。循环被展开后可以提升大数据级的处理速度。
还有一种更快的 Duff 装置技术,它将 do-while 循环分为两个单独的循环:
//更快的 Duff
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 {//主循环(调用 8 次)
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);
剩余的计算会在一个初始循环中进行,但额外的元素都被处理后,再进入主循环,这个方法会被原方法快 40%。
展开循环会有额外的开销,所以对于小数据集来说,可能得不偿失。建议只在大数据集中实践展开循环技术。
2.4 避免双重解释
如果想用 JavaScript 来解析 JavaScript 的时候会存在双重解释的性能惩罚:
//对代码求值(避免)
eval("alert('Hello world!')");
//创建新函数(避免)
var sayHi = new Function("alert('Hello world!')");
//设置超时(避免)
setTimeout("alert('Hello world!')", 500);
因为代码是包含在字符串中的,所以 JavaScript 在运行的同时会启动一个新的解析器来解析新的代码,而这有着不容忽视的开销,所以比直接的解析慢的多。上面的示例代码可以这样优化:
//修正
alert('Hello world!');
var sayHi = function () {
alert('Hello world!');
};
setTimeout(function () {
alert('Hello world!');
}, 500);
尽可能地避免出现需要 JavaScript 解析的字符串哦O(∩_∩)O~
2.5 其他注意事项
- 原生方法较快:原生方法使用 C/C++ 之类的编译型语言写的,所以要比 JavaScript 快很多,比如 Math 对象中可以找到很多复杂的数学运算方法,它比任何用 JavaScript 写的同样的方法快的多。
- Switch 语句较快:一系列复杂的 if 语句,可以转换为单个 switch 语句,把 case 语句按照最可能发生到最不可能发生的顺序进行组织,就能得到更快的代码。
- 位运算符较快:选择性地使用位运算符可以极大地提升复杂计算的性能,诸如取模、逻辑与和逻辑或都可以考虑使用位运算符来替换实现。
3 最小化语句数
完成多个操作的单个语句比完成单个操作的多个语句执行的快。所以可以找出可以组合在一起的语句,以减少脚本的整体执行时间。
3.1 多个变量声明
//多个变量声明
var count = 5;
var color = "blue";
var values = [1, 2, 3];
var now = new Date();
可以把所有的变量使用单个 var 语句来声明:
//一个语句
var count = 5,
color = "blue",
values = [1, 2, 3],
now = new Date();
这种优化很容易实现,它被单个变量的声明快很多哦O(∩_∩)O~
3.2 插入迭代值
当使用迭代值时,请尽可能地合并语句。
//插入迭代值
var name = values[i];
i++;
优化:
var name = values[i++];
3.3 使用数组和对象字面量
//浪费
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//浪费
ver
person = new Object();
person.name = "deniro";
person.age = 29;
person.sayName = function () {
console.log(this.name);
};
字面量可以把这些操作放在一个语句中完成:
var values = [123, 456, 789];
var person = {
name: "deniro",
age: 29,
sayName: function () {
console.log(this.name);
}
};
请尽量使用数组和对象字面量来消除不必要的语句哦O(∩_∩)O~
注意:在 IE6 以及更早版本中,使用字面量会有微小的性能惩罚,但在 IE7 已经解决了这个问题。
4 优化 DOM 交互
DOM 操作与交互要消耗大量的时间,因为它们往往需要渲染页面的一部分甚至是整个页面。
4.1 最小化现场更新
现场更新指的是对用户所看到的页面进行更新。每一次变化都有一个性能惩罚,因为浏览器可能需要重新计算很多容器的尺寸,以便实现更新。现场更新执行的越多,代码执行的时间就越长。
//20 个现场更新
var list = document.getElementById("myList"),
item,
i;
for (i = 0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
这段代码位列表添加了 10 个条目。每个条目添加时,都会有 2 次现场更新:
- 添加
<li>
元素。 - 为
<li>
元素添加文本节点。
所以总共进行了 20 个现场更新!
有两种方法可以解决这个性能瓶颈:
- 从页面移除列表,然后更新,最后再把列表插回原来的位置。这个方法的问题是,每次页面更新时会出现不必要的闪烁。
- 使用文档碎片来构建 DOM 结构,然后再添加到 List 元素中:
//使用文档碎片(1 个现场更新)
var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;
for (i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
文档碎片利用一个临时占位符来放置新添加的条目,最后再添加到列表中。使用 appendChild 方法传入碎片时,只有碎片的子节点会被添加到目标中哦O(∩_∩)O~
4.2 使用 innerHTML
有两种创建 DOM 节点的方法:
- DOM 方法:createElement()、appendChild() 等。
- innerHTML。
对于小的 DOM,两种方法效率差不多。但对于大的 DOM,使用 innerHTML 方法会快的多。
把 innerHTML 设置为某个值时,后台会创建一个 HTML 解析器,然后使用内部的 DOM 调用来创建 DOM 结构。因为内部方法是事先编译后的,所以执行会快很多:
var list = document.getElementById("myList"),
html = "",
i;
for (i = 0; i < 10; i++) {
html += "<li>Item " + i + "</li>";
}
list.innerHTML = html;
注意要最小化使用 innerHTML 的次数:
//使用 innerHTML 的次数太多了
var list = document.getElementById("myList"),
i;
for (i = 0; i < 10; i++) {
list.innerHTML += "<li>Item " + i + "</li>";//避免!!!
}
每次循环都调用一次 innerHTML,这是及其低效的!记住,调用一次 innerHTML,也相当于一次现场更新。所以我们要先构建好字符串,然后再一次性调用 innerHTML。
4.3 使用事件代理(委托)
大多数的 web 应用会在与用户的交互上用到大量的事件处理程序,而事件处理程序的数量和页面响应的用户交互速度之间存在负相关,所以最好使用事件委托。
事件委托利用事件冒泡,在更高层的地方来处理多个目标事件。如果有可能,尽量在文档级别上附加事件处理程序。
4.4 注意 HTMLCollection
记住,每次访问 HTMLCollection,都是在文档上进行一次查询,这个开销很高!所以我们要最小化 HTMLCollection 的访问次数,来改善脚本的执行性能。
var images = document.getElementsByTagName("img"),
image,
i, len;
for (i = 0, len = images.length; i < len; i++) {
image = images[i];
//处理
}
这里把 images.length 存入变量 len,避免每次循环都去访问 HTMLCollection 的 length。而且,我们还添加了 image 变量用于保存当前的图像,从而在循环内避免再次访问 images 的 HTMLCollection!
以下情况会返回 HTMLCollection 对象,记住它们吧:
- 调用了 getElementsByTagName()。
- 获取元素的 childNodes 属性。
- 获取元素的 attributes 属性。
- 访问了特殊集合:document.forms、document.images 等等。
合理使用 HTMLCollection,可以极大地提升代码的执行速度。