Javascript 性能优化

Javascript最初是解释型语言,现在,主流浏览器内置的Javascript引擎基本上都实现了Javascript的编译执行,即使如此,我们仍需要优化自己写的Javascript代码,以获得最佳性能。

注意作用域

避免全局作用域

在之前的文章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创建了自己的作用域,相当于增加了作用域内部查找变量的深度
    举一个例子:
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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容