Javascript 性能优化

Javascript最初是解释型语言,如今,主流浏览器内置的Javascript引擎基本上都实现了Javascript的编译执行,即便如此,咱们仍须要优化本身写的Javascript代码,以得到最佳性能。html

注意做用域

避免全局做用域

在以前的文章Javascript 变量、做用域和内存问题提到过,因为访问变量须要在做用域链上进行查找,相比于局部变量,访问全局变量的开销更大,所以如下代码:算法

var person = {
    name: "Sue",
    hobbies: ["Yoga", "Jogging"]
};
function hobby() {
    for(let i=0; i<person.hobbies.length; i++) {
        console.log(person.hobbies[i]);
    }
}

能够进行以下优化:数组

function hobby() {
    let hobbies = person.hobbies;
    for(let i=0; i<hobbies.length; i++) {
        console.log(hobbies[i]);
    }
}

把须要频繁访问的全局变量赋值到局部变量中,能够减少查找深度,进而优化性能。
固然,上述优化过的代码仍然有不足的地方,后面的部分会提到。浏览器

避免使用with

为何避免使用with?性能优化

  1. with并非必须的,使用局部变量能够达到一样的目的
  2. with建立了本身的做用域,至关于增长了做用域内部查找变量的深度

举一个例子:app

function test() {
    var innerW = "";
    var outerW = "";
    with(window) {
        innerW = innerWidth;
        outerW = outerWidth;
    }
    return "Inner W: " + innerW + ", Outer W: " + outerW;
}
test()
// "Inner W: 780, Outer W: 795"

上述代码中,with做用域减少了对全局变量window的查找深度,不过与此同时,也增长了做用域中局部变量innerWouterW的查找深度,功过相抵。
所以咱们不如使用局部变量替代with函数

function test() {
    var w = window;
    var innerW = w.innerWidth;
    var outerW = w.outerWidth;
    return "Inner W: " + innerW + ", Outer W: " + outerW;
}

上述代码仍然不是最优的。性能

算法复杂度

一下表格列出了几种算法复杂度:大数据

复杂度 名称 描述
O(1) 常数 不管多少值,执行时间恒定,好比使用简单值或访问存贮在变量中的值
O(lg n) 对数 总执行时间与值的数量相关,但不必定须要遍历每个值
O(n) 线性 总执行时间与值的数量线性相关
O(n2) 平方 总执行时间与值的数量相关,每一个值要获取n次

O(1)

若是咱们直接使用字面量,或者访问保存在变量中的值,时间复杂度为O(1),好比:优化

var value = 5;
var sum = 10 + value;

上述代码进行了三次常量查找,分别是5,10,value,这段代码总体复杂度为O(1)
访问数组也是时间复杂度为O(1)的操做,如下代码总体复杂度为O(1):

var values = [1, 2];
var sum = values[0] + values[1];

避免没必要要的属性查找

在对象上访问属性是一个O(n)的操做,Javascript 面向对象的程序设计(原型链与继承)文中提到过,访问对象中的属性时,须要沿着原型链追溯查找,属性查找越多,执行时间越长,好比:

var persons = ["Sue", "Jane", "Ben"];
for(let i=0; i<persons.length; i++) {
    console.log(persons[i]);
}

上述代码中,每次循环都会比较i<persons.length,为了不频繁的属性查找,能够进行以下优化:

var persons = ["Sue", "Jane", "Ben"];
for(let i=0, len = persons.length; i<len ; i++) {
    console.log(persons[i]);
}

即若是循环长度在循环开始时便可肯定,就将要循环的长度在初始化的时候声明为一个局部变量。

优化循环

因为循环时反复执行的代码,动辄上百次,所以优化循环时性能优化中很重要的部分。

减值迭代

为何要进行减值迭代,咱们比较以下两个循环:

var nums = [1, 2, 3, 4];
for(let i=0; i<nums.length; i++) {
    console.log(nums[i]);
}
for(let i=nums.length-1; i>-1; i--) {
    console.log(nums[i]);
}

两者有以下区别:

  1. 迭代顺序不一样
  2. 前者支持动态增减数组元素,后者不支持
  3. 后者性能优于前者,前者每次循环都会计算nums.length,频繁的属性查找下降性能

所以,出于性能的考虑,若是不在意顺序,迭代长度初始便可肯定,使用减值迭代更优。

简化终止条件

上述状况,咱们也能够不使用减值迭代,即像上文提到过的,在初始化时即将迭代长度赋值给一个局部变量。

简化循环体

循环体应最大程度地被优化,避免进行没必要要的密集的计算

使用while循环

为何使用while循环,咱们能够比较以下两个循环:

var len = nums.length;
for(let i=0; i<len; i++) {
    console.log(nums[i]);
}
var i = nums.length ;
while(--len > -1) {
    console.log(nums[len]);
}

以上两个循环有一个很明显的不一样点:while循环将每次循环终止条件的判断和index的自增合并为一个语句,在后续部分会讲解语句数量与性能优化的关系。

展开循环

因为创建循环和处理终止条件须要额外的开销,所以若是循环次数比较少,并且能够肯定,咱们能够将其展开,好比:

process(nums[0]);
process(nums[1]);

若是迭代次数不能事先肯定,可使用Duff装置,其中比较著名的是Andrew B. King提出的一种Duff技术,经过计算迭代次数是否为8的倍数将循环展开,将“零头”与“整数”分红两个单独的do-while循环,在处理大数据集时优化效果显著:

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);

避免双重解释

eval() Function() setTimeout()能够传入字符串,Javascript引擎会将其解析成能够执行的代码,意味着,Javascript执行到这里须要额外开一个解释器来解析字符串,会明显下降性能,所以:

  1. 尽可能避免使用eval()
  2. 避免使用Function构造函数,用通常function来代替
  3. setTimeout()传入函数做为参数

其余

使用原生方法

原生方法都是用C/C++之类的编译语言写出来的,比Javascript快得多。

使用switch语句

多个if-else能够转换为switch语句,还能够按照最可能到最不可能排序case

使用位运算符
当进行数学运算的时候,位运算操做要比任何布尔运算或者算数运算快。选择性地用位运算替换算数运算能够极大提高复杂计算的性能。诸如取模,逻辑与和逻辑或均可
以考虑用位运算来替换。

书中的这段话笔者表示不能理解,因为使用&& ||作逻辑判断时,有的时候只须要求得第一个表达式的结果即可以结束运算,而& |不管如何都要求得两个表达式的结果才能够结束运算,所以后者的性能没有占太大优点。
这里,补充一下位运算符如何发挥逻辑运算符的功能,首先看几个例子:

7 === 7 & 6 === 6
1
7 === 7 & 5 === 4
0
7 === 7 | 6 ===6
1
7 === 7 | 7 ===6
1
7 === 6 | 6 === 5
0

也许你会恍然大悟,位运算符并无产生truefalse,它只是利用了Number(true) === 1 Number(false) === 0 Boolean(1) === true Boolean(0) === false

最小化语句数

Javascript代码中的语句数量会影响执行的速度,尽可能组合语句,能够减小脚本的执行时间。

多个变量声明

当咱们须要声明多个变量,好比:

var name = "";
var age = 18;
var hobbies = [];

能够作以下优化:

var name = "",
    age = 18,
    hobbies = [];

合并迭代值

上文中咱们提到一个例子,使用while循环能够合并自减和判断终止条件,咱们还能够换一种写法:

var i = nums.length ;
while(len > -1) {
    console.log(nums[len--]);
}

即将自减与使用index取值合并为一个语句。

使用字面量建立数组和对象

即将以下代码:

var array = new Array();
array[0] = 1;
array[1] = 2;

var person = new Object();
person.name = "Sue";
person.age = 18;

替换成:

var array = [1, 2];
var person = { name:"Sue", age:18 };

省了4行代码。

优化DOM操做

DOM操做是最拖累性能的一方面,优化DOM操做能够显著提升性能。

最小化现场更新的次数

若是咱们要修改的DOM已经显示在页面,那么咱们就是在作现场更新,因为每次更新浏览器都要从新计算,从新渲染,很是消耗性能,所以咱们应该最小化现场更新的次数,好比咱们要向页面添加一个列表:

var body = document.getElementsByTagName("body")[0];
for(let i=0; i<10; i++) {
    item = document.createElement("span");
    body.appendChild(item);
    item.appendChild(document.createTextNode("Item" + i));
}

每次循环时都会进行两次现场更新,添加div,为div添加文字,总共须要20次现场更新,页面要重绘20次。
现场更新的性能瓶颈不在于更新的大小,而在于更新的次数,所以,咱们能够将全部的更新一次绘制到页面上,有如下两个方法:

文档片断

可使用文档片断先收集好要添加的元素,最后在父节点上调用appendChild()将片断的子节点添加到父节点中,注意,片断自己不会被添加。

<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
            <div id="child">this</div>
        </div>
        <script>
            var container = document.getElementById("container"),
                fragment = document.createDocumentFragment(),
                item,
                i;
            for (i=0; i < 10; i++) {
              item = document.createElement("li");
              fragment.appendChild(item);
              item.appendChild(document.createTextNode("Item " + i));
            }
            container.appendChild(fragment);
        </script>
    </body>
</html>
innerHTML

使用innerHTML与使用诸如createElement() appendChild()方法有一个显著的区别,前者使用内部的DOM来建立DOM结构,后者使用JavaScript的DOM来建立DOM结构,前者要快得多,以前的例子用innerHTML改写为:

var ul = document.getElementById("ul"),
    innerHTML = "";
for(let i=0; i<10; i++) {
    innerHTML += "<li>Item " + i + "</li>";
}
ul.innerHTML = innerHTML;

整合冒泡事件处理

页面上的事件处理程序数量与页面相应用户交互的速度之间存在负相关,具体缘由有多方面:

  1. 建立函数会占用内存
  2. 绑定事件处理方法时,须要访问DOM

所以对于冒泡事件,尽量由父元素甚至祖先元素代子元素处理,这样一个事件处理方法能够负责多个目标的事件处理,好比:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
            <div id="child">this</div>
        </div>
        <script>
            var container = document.getElementById("container");
            container.addEventListener("click", function(e) {
                switch(e.target.id) {
                    case "container":
                        console.log("container clicked");
                        break;
                    case "child":
                        console.log("child clicked");
                        break;
                }
            },false);
        </script>
    </body>
</html>

注意HTMLCollection

访问HTMLCollection的代价很是昂贵。
下面的每一个项目(以及它们指定的属性)都返回 HTMLCollection:

  1. Document (images, applets, links, forms, anchors)
  2. form (elements)
  3. map (areas)
  4. select (options)
  5. table (rows, tBodies)
  6. tableSection (rows)
  7. row (cells)
相关文章
相关标签/搜索