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)
相關文章
相關標籤/搜索