JS性能优化
1. 作用域相关
01 避免全局查找
优化经验:只要函数中有引用超过两次的全局对象,就应该把这个对象保存为一个局部变量。
原因:如果定义了一个全局的变量,那么在局部作用域查找这个全局变量时,会经历作用域链查找,因此全局变量与局部变量相比,是更费时间的。
代码分析:
第一个函数三次引用了全局document对象,如果页面图片非常多,那么for循环就会引用上百次等,每次都要遍历一次作用域链。
第二个函数优化了这一点。在局部保存了document对象的引用,因此只会查找一次作用域链,就优化了性能。
//优化前
function updateUI() {
let imgs = document.getElementsByTagName("img");
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = '${document.title} image ${i}';
}
let msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
//优化后
function updateUI() {
let doc = document;
let imgs = doc.getElementsByTagName("img");
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = '${doc.title} image ${i}';
}
let msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
02 避免使用with语句
优化经验:不要使用with语句。
原因:with语句会创建自己的作用域,因此会加长代码的作用域链,因此用with语句,比不用with要多查一步作用域链,所以with更慢。
with使用示例(不建议用):
function updateBody() {
with(document.body){
console.log(tagName);
}
}
2. js方法相关
01 避免不必要的属性查找
优化经验:只要使用某个object
深层属性超过一次,就应该将其保存在局部变量中
原因:在访问对象时,是要查找原型链的,算法复杂度是O(n),查找的属性越多,执行时间就越长。
代码分析:第一段代码,有六次属性查找,有几个点就有查找几次。因此优化的话,就应该避免多次使用一个object深层对象,应该保存到局部变量中。
第二段代码,优化完成后就只有四次属性查找,节约了33%
想一下,如果脚本特别大的情况,这种的提升就非常明显能改变性能了。
//优化前
let query = window.location.href.substring(window.location.href.indexOf("?"));
//优化后
let url = window.location.href;
let query = url.substring(url.indexOf("?"));
02 优化循环
优化经验和原因:
(1)简化优化条件
。因为每次循环都会计算终止条件,所以它应该可以更快。
(2)简化循环体
。确保循环中去除可以在循环外部计算的方法。
(3)使用后测试循环
。do-while
是后测试循环,避免了对终止条件的初始评估。for、while循环是先测试循环。
优化分析:
第一个循环条件中,values.length被查询了i次,复杂度O(n)。
第二个循环条件中,values.length被查询了一次,复杂度O(1),因此性能提升了。
第三个属于后测试循环,values.length被查询了一次,复杂度O(1),并且 --i 和i>=0 合并成了一个。
//优化前
for (let i = 0; i < values.length; i++) {
process(values[i]);
}
//优化后
for (let i = values.length - 1; i >= 0; i--) {
process(values[i]);
}
//后测试循环
let i = values.length-1;
if (i > -1) {
do {
process(values[i]);
}while(--i >= 0);
}
03 展开循环
优化经验:如果循环的次数是有限的,多次调用函数比循环调用函数会更快。
原因:这样可以节约创建循环、计算终止循环条件消耗,让代码更快。
其他:如果不能预知循环次数,那么可以用一种Duff's Device
(达夫设备)的技术。达夫设备的基本思路是以8的倍数作为迭代次数从而将循环展开为一系列语句。
代码示例:
// 优化后
process(values[0]);
process(values[1]);
process(values[2]);
04 避免重复解释
优化经验:避免写一些字符串,里面带有js代码。
原因:如果代码包含在字符串里,js运行时必须启动解析器实例来解析这些字符串的代码,而实例化解析器很费时间。
代码示例:
// 对代码求值:不要
eval("console.log('Hello world!')");
// 创建新函数:不要
let sayHi = new Function("console.log('Hello world!')");
// 设置超时函数:不要
setTimeout("console.log('Hello world!')", 500);
05 其他性能优化注意事项
1.原生方法很快。原生方法指的是使用C++或C编写的,不是js写的方法,比如Math这种。
2.switch语句很快,如果代码中有复杂的if-else语句,将其转换成switch,switch语句中把最可能的放前面,不太可能的放后面。
3.位操作很快,在执行数学运算操作时,位操作比任何布尔值或数值计算更快。对于复杂计算来讲,可以较高提升效率。
3.语句最少优化
优化经验:声明多个变量时,都可以使用一个let语句声明。
原因:一次声明比多次声明变量,性能肯定要快。
01 多变量声明
//优化前
//四条语句浪费
let count = 5;
let color = "blue";
let values = [1,2,3];
let now = new Date();
//优化后
// 一条语句更好
let count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
02 插入迭代性值
优化经验:遇到递增或者递减的值,尽可能组合语句。
原因:两次执行语句可以 合并成一个语句
//优化前
let name = values[i];
i++;
//优化后
let name = values[i++]
03 使用数组和对象字面量
优化经验:创建一个数组或对象的时候,减少语句
//优化前
// 创建和初始化数组用了四条语句:浪费
let values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
// 创建和初始化对象用了四条语句:浪费
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function() {
console.log(this.name);
};
//优化后
// 一条语句创建并初始化数组
let values = [123, 456, 789];
// 一条语句创建并初始化对象
let person = {
name: "Nicholas",
age: 29,
sayName() {
console.log(this.name);
}
};
4.优化Dom
首先,操作dom为什么会慢?
原因1:dom中包含了数千条信息,每次访问dom,浏览器会重新计算数千项指标,才能执行更新。
原因2:dom属于v8引擎,js属于js引擎,js每次操作dom就意味着v8引擎和js引擎就进行一次通信,频繁的更新dom就是频繁的进行引擎间的通信,成本很高,性能代价很大。
01 使用createDocumentFragement
使用文档片段构建dom结构, 在对dom修改的时候,只修改创建的文档片段,最终一次性添加到dom中.
这样修改后,完成这样的操作只需要触发一次实时更新。
let list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item;
for (let i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
02 使用innerHTML
优化经验:在页面中,创建DOM节点的方式有两种,使用createElement,和使用innerHTML。
原因:对于少量更新,两者区别不大,对于大量DOM更新,使用innerHTML要快很多。
调用一次innerHTML,后台其实也是在创建原生DOM,这种方式要比js中的创建要快,因为这是原生,js中属于解释型代码。
let list = document.getElementById("myList"),
html = "";
for (let i = 0; i < 10; i++) {
html += '<li>Item ${i}</li>';
}
list.innerHTML = html;
03 使用事件委托
就是把事件注册到父级,而不是注册到每个单独的子元素,减少触发事件监听。
04 注意HTMlCollection
最常见的就是去访问一个数组的length,比如for循环中,每次循环执行,都去判断数组的长度,接着就会去查询文档,这个查询是很耗时的,来看优化后的例子。这里我们吧length赋给了len,因此只查询了一次。
优化思路:目标就是减少查询次数,把这些耗时的东西存储到临时变量中。
let images = document.getElementsByTagName("img");
for (let i = 0, len = images.length; i < len; i++) {
// 处理
}
总结
以上就是关于js的一些性能优化中需要注意的事项,在ES6中还有那么多的语法,优化的方案还有很多,但最关键的还是优化的思想,后续开发中重视起来对每一行代码的优化,是成为优秀的开发者重要的一环。