1. 程式人生 > >前端效能優化之 JavaScript

前端效能優化之 JavaScript

前言

本文為 《高效能 JavaScript》 讀書筆記,是利用中午休息時間、下班時間以及週末整理出來的,此書雖有點老舊,但談論的效能優化話題是每位同學必須理解和掌握的,業務響應速度直接影響使用者體驗。

一、載入和執行

大多數瀏覽器使用單程序處理 UI 更新和 JavaScript 執行等多個任務,而同一時間只能有一個任務被執行

指令碼位置

將所有script標籤放在頁面底部,緊靠</body>上方,以保證頁面指令碼執行之前完成解析

<html>
  <head> </head>
  <body>
    <p>Hello World</p>
    <!--  -->
    <script type="text/javascript" src="file.js"></script>
  </body>
</html>

defer & async

常規script指令碼瀏覽器會立即載入並執行,非同步載入使用asyncdefer
二者區別在於aysnc為無序,defer會非同步根據指令碼位置先後依次載入執行

<!-- file1、file2依次載入 -->
<script type="text/javascript" src="file1.js" defer></script>
<script type="text/javascript" src="file2.js" defer></script>
<!-- file1、file2無序載入 -->
<script type="text/javascript" src="file1.js" async></script>
<script type="text/javascript" src="file2.js" async></script>

動態指令碼

無論在何處啟動下載,檔案的下載和執行都不會阻塞其他頁面處理過程。你甚至可以將這些程式碼放在<head>部分而不會對其餘部分的頁面程式碼造成影響(除了用於下載檔案的 HTTP

連線)

var script = document.createElement("script");
script.type = "text/javascript";
script.src = "file1.js";
document.getElementsByTagName("head")[0].appendChild(script);

監聽載入函式

function loadScript(url, callback) {
  var script = document.createElement("script");
  script.type = "text/javascript";
  if (script.readyState) {
    //IE
    script.onreadystatechange = function() {
      if (script.readyState == "loaded" || script.readyState == "complete") {
        script.onreadystatechange = null;
        callback();
      }
    };
  } else {
    //Others
    script.onload = function() {
      callback();
    };
  }
  script.src = url;
  document.getElementsByTagName("head")[0].appendChild(script);
}

XHR 注入

前提條件為同域,此處與非同步載入一樣,只不過使用的是 XMLHttpRequest


總結

  • 將所有script標籤放在頁面底部,緊靠 body 關閉標籤上方,以保證頁面指令碼執行之前完成解析
  • 將指令碼成組打包,頁面 script 標籤越少載入越快,響應也就更迅速。不論外部指令碼檔案或者內聯程式碼都是如此

二、資料訪問

資料儲存在哪裡,關係到程式碼執行期間資料被檢索到的速度.每一種資料儲存位置都具有特定的讀寫操作負擔。大多數情況下,對一個直接量和一個區域性變數資料訪問的效能差異是微不足道的。

在 JavaScript 中有四種基本的資料訪問位置:

  • 直接量
    直接量僅僅代表自己,而不儲存於特定位置。 JavaScript 的直接量包括:字串,數字,布林值,物件,陣列,函式,正則表示式,具有特殊意義的空值,以及未定義
  • 變數
    使用 var / let 關鍵字建立用於儲存資料值
  • 陣列項
    具有數字索引,儲存一個 JavaScript 陣列物件
  • 物件成員
    具有字串索引,儲存一個 JavaScript 物件

總結

  • 直接量與區域性變數訪問速度非常快,陣列項和物件成員需要更長時間

  • 區域性變數比域外變數訪問速度快,因為它位於作用域鏈的第一個物件中。變數在作用域鏈的位置越深,訪問所需要的時間越長。全域性變數總是最慢的,因為它們總位於作用域鏈的最後一環。

  • 避免使用 with 表示式,因為它改變了執行期上下文的作用域鏈,謹慎對待 try-catch 表示式中 catch 子句,因為它具有同樣的效果

  • 巢狀物件成員會造成重大效能影響,儘量少用

  • 屬性在原型鏈中的位置越深,訪問速度越慢

  • 將物件成員、陣列項、域外變數存入區域性變數能提高 js 程式碼的效能

三、dom 程式設計

對 DOM 操作代價昂貴,在富網頁應用中通常是一個性能瓶頸。通常處理以下三點

  • 訪問和修改 DOM 元素
  • 修改 DOM 元素的樣式,造成重繪和重新排版
  • 通過 DOM 事件處理使用者響應

    一個很形象的比喻是把 DOM 看成一個島嶼,把 JavaScript(ECMAScript)看成另一個島嶼,兩者之間以一座收費橋連線(參見 John Hrvatin,微軟,MIX09,http://videos.visitmix.com/MIX09/T53F)。每次 ECMAScript 需要訪問 DOM 時,你需要過橋,交一次“過橋費”。你操作 DOM 次數越多,費用就越高。一般的建議是儘量減少過橋次數,努力停留在 ECMAScript 島上。

DOM 訪問和修改

訪問或修改元素最壞的情況是使用迴圈執行此操作,特別是在 HTML 集合中使用迴圈

function innerHTMLLoop() {
  for (var count = 0; count < 15000; count++) {
    document.getElementById("here").innerHTML += "a";
  }
}

此函式在迴圈中更新頁面內容。這段程式碼的問題是,在每次迴圈單元中都對 DOM 元素訪問兩次:一次
讀取 innerHTML 屬效能容,另一次寫入它


優化如下

function innerHTMLLoop2() {
  var content = "";
  for (var count = 0; count < 15000; count++) {
    content += "a";
  }
  document.getElementById("here").innerHTML += content;
}

你訪問 DOM 越多,程式碼的執行速度就越慢。因此,一般經驗法則是:輕輕地觸控 DOM,並儘量保持在 ECMAScript 範圍內

節點克隆

使用 DOM 方法更新頁面內容的另一個途徑是克隆已有 DOM 元素,而不是建立新的——即使用 element.cloneNode()(element 是一個已存在的節點)代替 document.createElement();

當佈局和幾何改變時發生重排版,下述情況會發生:

  • 新增或刪除可見的 DOM 元素
  • 元素位置改變
  • 元素尺寸改變(邊距、填充、邊框寬度、寬、高等屬性)
  • 內容改變(文字或者圖片被另一個不同尺寸的所替代)
  • 最初的頁面渲染
  • 瀏覽器視窗尺寸改變

減少重排次數

  • 改變 display 屬性,臨時從文件上移除然後再恢復
  • 在文件之外建立並更新一個文件片段,然後將它進行附加
  • 先建立更新節點的副本,再操作副本,最後用副本更新老節點

總結

  • 最小化 DOM 訪問,在 JavaScript 端做盡可能多的事情
  • 在反覆訪問的地方使用區域性變數存放 dom 引用
  • 謹慎處理 HTML 集合,因為它們表現‘存在性’,總對底層文件重新查詢。將 length 屬性快取到一個變數中,在迭代中使用這個變數。如果經常操作這個集合,可以將集合拷貝到陣列中
  • 如果可以,使用速度更快的 API,比如 document.querySelectorAll()和 firstElementChild()
  • 注意重繪和重排,批量修改風格,離線操作 DOM,快取或減少對佈局資訊的訪問
  • 動畫中使用絕對座標,使用拖放代理
  • 使用事件託管技術中的最小化事件控制代碼數量

四、演算法與流程控制

程式碼整體結構是執行速度的決定因素之一。程式碼量少不一定執行快,程式碼量多,也不一定執行慢,效能損失與程式碼組織方式和具體問題解決辦法直接相關。

Loops

在大多數程式語言中,程式碼執行時間多數在迴圈中度過。在一系列程式設計模式中,迴圈是最常見的模式之一,提高效能必須控制好迴圈,死迴圈和長時間迴圈會嚴重影響使用者體驗。

Types of Loops

  • for
  • while
  • do while
  • for in

前三種迴圈幾乎所有程式語言都能通用,for in 迴圈遍歷物件命名屬性(包括自有屬性和原型屬性)

Loop Performance

迴圈效能爭論的源頭是應當選用哪種迴圈,在 JS 中 for-in 比其他迴圈明顯要慢(每次迭代都要搜尋例項或原型屬性),除非對數目不詳的物件屬性進行操作,否則避免使用 for-in。除開 for-in,選擇迴圈應當基於需求而不是效能

減少每次迭代的操作總數可以大幅提高迴圈的整體效能

優化迴圈:

  • 減少物件成員和陣列項的查詢,比如快取陣列長度,避免每次查詢陣列 length 屬性
  • 倒序迴圈是程式語言中常用的效能優化方法

程式設計中經常會聽到此說法,現在來驗證一下,測試樣例

var arr = [];
for (var i = 0; i < 100000000; i++) {
  arr[i] = i;
}
var start = +new Date();
for (var j = arr.length; j > -1; j--) {
  arr[j] = j;
}
console.log("倒序迴圈耗時:%s ms", Date.now() - start); //約180 ms
var start = +new Date();
for (var j = 0; j < arr.length; j++) {
  arr[j] = j;
}
console.log("正序序迴圈耗時:%s ms", Date.now() - start); //約788 ms

基於函式的迭代

儘管基於函式的迭代顯得更加便利,它還是比基於迴圈的迭代要慢一些。每個陣列項要關聯額外的函式呼叫是造成速度慢的原因。在所有情況下,基於函式的迭代佔用時間是基於迴圈的迭代的八倍,因此在關注執行時間的情況下它並不是一個合適的辦法。

條件表示式

if-else VS switch

使用 if-else 或者 switch 的流行理論是基於測試條件的數量:條件數量較大,傾向使用 switch,更易於閱讀
當條件體增加時,if-else 效能負擔增加的程度比 switch 更多。
一般來說,if-else 適用於判斷兩個離散的值或者幾個不同的值域,如果判斷條件較多 switch 表示式將是更理想的選擇

優化 if-else

  • 最小化找到正確分支:將最常見的條件放在首位

  • 查表法 當使用查表法時,必須完全消除所有條件判斷,操作轉換成一個數組項查詢或者一個物件成員查詢。

遞迴

會受瀏覽器呼叫棧大小的限制

迭代

任何可以用遞迴實現的演算法可以用迭代實現。使用優化的迴圈替代長時間執行的遞迴函式可以提高效能,因為執行一個迴圈比反覆呼叫一個函式的開銷要低

斐波那契

function fibonacci(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

製表

//製表
function memorize(fundamental, cache) {
  cache = cache || {};
  var shell = function(args) {
    if (!cache.hasOwnProperty(args)) {
      cache[args] = fundamental(args);
    }
    return cache[args];
  };
  return shell;
}
//動態規劃
function fibonacciOptimize(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  var current = 2;
  var previous = 1;
  for (var i = 3; i <= n; i++) {
    var temp = current;
    current = previous + current;
    previous = temp;
  }
  return current;
}
//計算階乘
var res1 = fibonacci(40);
var res2 = memorize(fibonacci)(40);
var res3 = fibonacciOptimize(40);
//計算出來的res3優於res2,res2優於res1

總結

執行程式碼的總量越大,優化帶來的效能提升越明顯
正如其他程式語言,程式碼的寫法與演算法選用影響 JS 的執行時間,與其他程式語言不同,JS 可用資源有限,所以優化固然重要

  • for, while, do while 迴圈的效能特性相似,誰也不比誰更快或更慢
  • 除非要迭代遍歷一個屬性未知的物件,否則不要使用 for-in 迴圈
  • 改善迴圈的最佳方式減少每次迭代中的運算量,並減少迴圈迭代次數
  • 一般來說 switch 總比 if-else 更快,但總不是最好的解決方法
  • 當判斷條件較多,查表法優於 if-else 和 switch
  • 瀏覽器的呼叫棧大小限制了遞迴演算法在 js 中的應用,棧溢位導致其他程式碼不能正常執行
  • 如果遇到棧溢位,將方法修改為製表法,可以避免重複工作

五、字串和正則表示式 String And Regular Expression

在 JS 中,正則是必不可少的東西,它的重要性遠遠超過煩瑣的字串處理

字串連結 Stirng Concatenation

字串連線表現出驚人的效能緊張。通常一個任務通過一個迴圈,向字串末尾不斷地新增內容,來建立一個字串(例如,建立一個 HTML 表或者一個 XML 文件),但此類處理在一些瀏覽器上表現糟糕而遭人痛恨

Method Example
+ str = 'a' + 'b' + 'c';
+= str = 'a';
str += 'b';
str += 'c';
array.join() str = ['a','b','c'].join('');
string.concat() str = 'a';
str = str.concat('b', 'c');

當連線少量的字串,上述的方式都很快,可根據自己的習慣使用;
當合並字串的長度和數量增加之後,有些函式就開始發揮其作用了

+ & +=

str += "a" + "b";

此程式碼執行時,發生四個步驟

  1. 記憶體中建立了一個臨時字串
  2. 臨時字串的值被賦予'ab'
  3. 臨時串與 str 進行連線
  4. 將結果賦予 str

下面的程式碼通過兩個離散的表示式直接將內容附加在 str 上避免了臨時字串

str += "a";
str += "b";

事實上用一行程式碼就可以解決

str = str + "a" + "b";

賦值表示式以 str 開頭,一次追加一個字串,從左至右依次連線。如果改變了連線順序(例如:str = 'a' + str + 'b'),你會失去這種優化,這與瀏覽器合併字串時分配記憶體的方法有關。除 IE 外,瀏覽器嘗試擴充套件表示式左端字串的記憶體,然後簡單地將第二個字串拷貝到它的尾部。如果在一個迴圈中,基本字串在左端,可以避免多次複製一個越來越大的基本字串。

Array.prototype.join

Array.prototype.join 將陣列的所有元素合併成一個字串,並在每個元素之間插入一個分隔符字串。若傳遞一個空字串,可將陣列的所有元素簡單的拼接起來

var start = Date.now();
var str = "I'm a thirty-five character string.",
  newStr = "",
  appends = 5000000;
while (appends--) {
  newStr += str;
}
var time = Date.now() - start;
console.log("耗時:" + time + "ms"); //耗時:1360ms
var start = Date.now();
var str = "I'm a thirty-five character string.",
  strs = [],
  newStr = "",
  appends = 5000000;
while (appends--) {
  strs[strs.length] = str;
}
newStr = strs.join("");
var time = Date.now() - start;
console.log("耗時:" + time + "ms"); //耗時:414ms

這一難以置信的改進結果是因為避免了重複的記憶體分配和拷貝越來越大的字串。

String.prototype.concat

原生字串連線函式接受任意數目的引數,並將每一個引數都追加在呼叫函式的字串上

var str = str.concat(s1);
var str = str.concat(s1, s2, s3);
var str = String.prototype.concat.apply(str, array);

大多數情況下 concat 比簡單的+或+=慢一些

Regular Expression Optimization 正則表示式優化

許多因素影響正則表示式的效率,首先,正則適配的文字千差萬別,部分匹配時比完全不匹配所用的時間要長,每種瀏覽器的正則引擎也有不同的內部優化

正則表示式工作原理

  1. 編譯
    當你建立了一個正則表示式物件之後(使用一個正則表示式直接量或者 RegExp 構造器),瀏覽器檢查你的模板有沒有錯誤,然後將它轉換成一個本機程式碼例程,用執行匹配工作。如果你將正則表示式賦給一個變數,你可以避免重複執行此步驟。

  2. 設定起始位置
    當一個正則表示式投入使用時,首先要確定目標字串中開始搜尋的位置。它是字串的起始位置,或者由正則表示式的 lastIndex 屬性指定,但是當它從第四步返回到這裡的時候(因為嘗試匹配失敗),此位置將位於最後一次嘗試起始位置推後一個字元的位置上

  3. 匹配每個正則表示式的字元
    正則表示式一旦找好起始位置,它將一個一個地掃描目標文字和正則表示式模板。當一個特定字元匹配失敗時,正則表示式將試圖回溯到掃描之前的位置上,然後進入正則表示式其他可能的路徑上

  4. 匹配成功或失敗
    如果在字串的當前位置上發現一個完全匹配,那麼正則表示式宣佈成功。如果正則表示式的所有可能路徑都嘗試過了,但是沒有成功地匹配,那麼正則表示式引擎回到第二步,從字串的下一個字元重新嘗試。只有字串中的每個字元(以及最後一個字元後面的位置)都經歷了這樣的過程之後,還沒有成功匹配,那麼正則表示式就宣佈徹底失敗。

理解回溯

在大多數現代正則表示式實現中(包括 JavaScript 所需的),回溯是匹配過程的基本組成部分。它很大程度上也是正則表示式如此美好和強大的根源。然而,回溯計算代價昂貴,如果你不夠小心的話容易失控。雖然回溯是整體效能的唯一因素,理解它的工作原理,以及如何減少使用頻率,可能是編寫高效正則表示式最重要的關鍵點。

正則表示式匹配過程

  • 當一個正則表示式掃描目標字串時,它從左到右逐個掃描正則表示式的組成部分,在每個位置上測試能不能找到一個匹配。對於每一個量詞和分支,都必須決定如何繼續進行。如果是一個量詞(諸如*,+?,或者{2,}),正則表示式必須決定何時嘗試匹配更多的字元;如果遇到分支(通過|操作符),它必須從這些選項中選擇一個進行嘗試。
  • 每當正則表示式做出這樣的決定,如果有必要的話,它會記住另一個選項,以備將來返回後使用。如果所選方案匹配成功,正則表示式將繼續掃描正則表示式模板,如果其餘部分匹配也成功了,那麼匹配就結束了。但是如果所選擇的方案未能發現相應匹配,或者後來的匹配也失敗了,正則表示式將回溯到最後一個決策點,然後在剩餘的選項中選擇一個。它繼續這樣下去,直到找到一個匹配,或者量詞和分支選項的所有可能的排列組合都嘗試失敗了,那麼它將放棄這一過程,然後移動到此過程開始位置的下一個字元上,重複此過程。

示例分析

/h(ello|appy) hippo/.test("hello there, happy hippo");

此正則表示式匹配“hello hippo”或“happy hippo”。測試一開始,它要查詢一個 h,目標字串的第一個字母恰好就是 h,它立刻就被找到了。接下來,子表示式(ello|appy)提供了兩個處理選項。正則表示式選擇最左邊的選項(分支選擇總是從左到右進行),檢查 ello 是否匹配字串的下一個字元。確實匹配,然後正則表示式又匹配了後面的空格。然而在這一點上它走進了死衚衕,因為 hippo 中的 h 不能匹配字串中的下一個字母 t。此時正則表示式還不能放棄,因為它還沒有嘗試過所有的選擇,隨後它回溯到最後一個檢查點(在它匹配了首字母 h 之後的那個位置上)並嘗試匹配第二個分支選項。但是沒有成功,而且也沒有更多的選項了,所以正則表示式認為從字串的第一個字元開始匹配是不能成功的,因此它從第二個字元開始,重新進行查詢。它沒有找到 h,所以就繼續向後找,直到第 14 個字母才找到,它匹配 happy 的那個 h。然後它再次進入分支過程。這次 ello 未能匹配,但是回溯之後第二次分支過程中,它匹配了整個字串“happy hippo”(如圖 5-4)。匹配成功了。

回溯失控

當一個正則表示式佔用瀏覽器上秒,上分鐘或者更長時間時,問題原因很可能是回溯失控。正則表示式處理慢往往是因為匹配失敗過程慢,而不是匹配成功過程慢。

var reg = /<html>[\s\S]*?<head>[\s\S]*?<title>[\s\S]*?<\/title>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/;
//優化如下
var regOptimize = /<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/;

現在如果沒有尾隨的那麼最後一個[\s\S]*?將擴充套件至字串結束,正則表示式將立刻失敗因為沒有回溯點可以返回

提高正則表示式效率的更多方法

  • 關注如何讓匹配更快失敗
  • 正則表示式以簡單的,必需的字元開始
  • 編寫量詞模板,使它們後面的字元互相排斥
  • 減少分支的數量,縮小它們的範圍
  • 使用非捕獲組
  • 捕獲感興趣的文字,減少後處理
  • 暴露所需的字元
  • 使用適當的量詞
  • 將正則表示式賦給變數,以重用它們
  • 將複雜的正則表示式拆分為簡單的片斷

什麼時候不應該使用正則表示式

var endsWithSemicolon = /;$/.test(str);

你可能覺得很奇怪,雖說當前沒有哪個瀏覽器聰明到這個程度,能夠意識到這個正則表示式只能匹配字串的末尾。最終它們所做的將是一個一個地測試了整個字串。字串的長度越長(包含的分號越多),它佔用的時間也越長

var endsWithSemicolon = str.charAt(str.length - 1) == ";";

這種情況下,更好的辦法是跳過正則表示式所需的所有中間步驟,簡單地檢查最後一個字元是不是分號:

這個例子使用 charAt 函式在特定位置上讀取字元。字串函式 slice,substr,和 substring 可用於在特定位置上提取並檢查字串的值

所有這些字串操作函式速度都很快,當您搜尋那些不依賴正則表示式複雜特性的文字字串時,它們有助於您避免正則表示式帶來的效能開銷

字串修剪

正則表示式允許你用很少的程式碼實現一個修剪函式,這對 JavaScript 關心檔案大小的庫來說十分重要。可能最好的全面解決方案是使用兩個子表示式:一個用於去除頭部空格,另一個用於去除尾部空格。這樣處理簡單而迅速,特別是處理長字串時。

//方法 用正則表示式修剪
// trim1
String.prototype.trim = function() {
  return this.replace(/^\s+/, "").replace(/\s+$/, "");
};
//trim2
String.prototype.trim = function() {
  return this.replace(/^\s+|\s+$/g, "");
};
// trim 3
String.prototype.trim = function() {
  return this.replace(/^\s*([\s\S]*?)\s*$/, "$1");
};
// trim 4
String.prototype.trim = function() {
  return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1");
};
// trim 5
String.prototype.trim = function() {
  return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
};
//方法二 不使用正則表示式修剪
String.prototype.trim = function() {
  var start = 0;
  var end = this.length - 1;
  //ws 變數包括 ECMAScript 5 中定義的所有空白字元
  var ws =
    "\n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff";
  while (ws.indexOf(this.charAt(start)) > -1) {
    start++;
  }
  while (end > start && ws.indexOf(this.charAt(end)) > -1) {
    end--;
  }
  return this.slice(start, end + 1);
};
//方法三 混合解決方案
String.prototype.trim = function() {
  var str = this.replace(/^\s+/, ""),
    end = str.length - 1,
    ws = /\s/;
  while (ws.test(str.charAt(end))) {
    end--;
  }
  return str.slice(0, end + 1);
};

簡單地使用兩個子正則表示式在所有瀏覽器上處理不同內容和長度的字串時,均表現出穩定的效能。因此它可以說是最全面的解決方案。混合解決方案在處理長字串時特別快,其代價是程式碼稍長,在某些瀏覽器上處理尾部長空格時存在弱點

總結

  • 使用簡單的+和+=取代陣列聯合,可避免(產生)不必要的中間字串
  • 當連線數量巨大或尺寸巨大的字串時,使用陣列聯合
  • 使相鄰字元互斥,避免巢狀量詞對一個字串的相同部分多次匹配,通過重複利用前瞻操作的原子特性去除不必要的回溯

六、響應介面

使用者傾向於重複嘗試這些不發生明顯變化的動作,所以確保網頁應用程式的響應速度也是一個重要的效能關注點

瀏覽器 UI 執行緒

JavaScript 和 UI 更新共享的程序通常被稱作瀏覽器 UI 執行緒, UI 執行緒圍繞著一個簡單的佇列系統工作,任務被儲存到佇列中直至程序空閒。一旦空閒,佇列中的下一個任務將被檢索和執行。這些任務不是執行 JavaScript 程式碼,就是執行 UI 更新,包括重繪和重排版.
大多數瀏覽器在 JavaScript 執行時停止 UI 執行緒佇列中的任務,也就是說 JavaScript 任務必須儘快結束,以免對使用者體驗造成不良影響

Brendan Eich,JavaScript 的創造者,引用他的話說,“[JavaScript]運行了整整幾秒鐘很可能是做錯了什麼……”

定時器基礎

定時器與 UI 執行緒互動的方式有助於分解長執行指令碼成為較短的片斷

定時器精度

所有瀏覽器試圖儘可能準確,但通常會發生幾毫秒滑移,或快或慢。正因為這個原因,定時器不可用於測量實際時間

總結

  • JavaScript 執行時間不應該超過 100 毫秒。過長的執行時間導致 UI 更新出現可察覺的延遲,從而對整體使用者體驗產生負面影響
  • JavaScript 執行期間,瀏覽器響應使用者互動的行為存在差異。無論如何,JavaScript 長時間執行將導致使用者體驗混亂和脫節。
  • 同一時間只有一個定時器存在,只有當這個定時器結束時才建立一個新的定時器。以這種方式使用定時器不會帶來效能問題
  • 定時器可用於安排程式碼推遲執行,它使得你可以將長執行指令碼分解成一系列較小的任務

七、Ajax

目前最常用的方法中,XMLHttpRequest(XHR)用來非同步收發資料。所有現代瀏覽器都能夠很好地支援它,而且能夠精細地控制傳送請求和資料接收。你可以向請求報文中新增任意的頭資訊和引數(包括 GET 和 POST),並讀取從伺服器返回的頭資訊,以及響應文字自身

請求資料

五種常用技術用於向伺服器請求資料

  • XMLHttpRequest (XHR)
  • Dynamic script tag insertion 動態指令碼標籤插入
  • iframes
  • Comet
  • Multipart XHR 多部分的 XHR

XMLHttpRequest

//封裝ajax
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status >= 200) {
    //
  }
};
xhr.open(type, url, true);
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(null);

動態指令碼標籤插入

傳送資料

  • XMLHttpRequest
  • 影象燈標

資料格式

通過 Douglas Crockford 的發明與推廣,JSON 是一個輕量級並易於解析的資料格式,它按照 JavaScript 物件和陣列字面語法所編寫

Ajax 效能嚮導

資料傳輸技術和資料格式

  • 快取資料
  • 設定 HTTP 頭
  • 本地儲存資料

總結

高效能 Ajax 包括:知道你專案的具體需求,選擇正確的資料格式和與之相配的傳輸技術

  • 減少請求數量,可合併 js 和 css 檔案
  • 縮短頁面的載入時間,在頁面其它內容載入之後,使用 Ajax 獲取少量重要檔案
  • JSON 是高效能 AJAX 的基礎,尤其在使用動態指令碼注入時
  • 學會何時使用一個健壯的 Ajax 庫,何時編寫自己的底層 Ajax 程式碼

封裝自己的 ajax 庫

(function(root) {
  root.MyAjax = (config = {}) => {
    let url = config.url;
    let type = config.type || "GET";
    let async = config.async || true;
    let headers = config.headers || [];
    let contentType = config.contentType || "application/json;charset=utf-8";
    let data = config.data;
    let dataType = config.dataType || "json";
    let successFn = config.success;
    let errorFn = config.error;
    let completeFn = config.complete;
    let xhr;
    if (window.XMLHttpRequest) {
      xhr = new XMLHttpRequest();
    } else {
      xhr = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          let rsp = xhr.responseText || xhr.responseXML;
          if (dataType === "json") {
            rsp = eval("(" + rsp + ")");
          }
          successFn(rsp, xhr.statusText, xhr);
        } else {
          errorFn(xhr.statusText, xhr);
        }
        if (completeFn) {
          completeFn(xhr.statusText, xhr);
        }
      }
    };
    xhr.open(type, url, async);
    //設定超時
    if (async) {
      xhr.timeout = config.timeout || 0;
    }
    //設定請求頭
    for (let i = 0; i < headers.length; ++i) {
      xhr.setRequestHeader(headers[i].name, headers[i].value);
    }
    xhr.setRequestHeader("Content-Type", contentType);
    //send
    if (
      typeof data == "object" &&
      contentType === "application/x-www-form-urlencoded"
    ) {
      let s = "";
      for (attr in data) {
        s += attr + "=" + data[attr] + "&";
      }
      if (s) {
        s = s.slice(0, s.length - 1);
      }
      xhr.send(s);
    } else {
      xhr.send(data);
    }
  };
})(window);

八、程式設計實踐

  • 避免二次評估,比如 eval,Function
  • 使用物件/陣列直接量
  • 不要重複工作
  • 延遲載入
  • 條件預載入
  • 使用速度快的部分
  • 位操作運算子
    四種位邏輯操作符
    • 位與
      比如判斷數奇偶
    num % 2 === 0; //取模與0進行判斷
    num & 1; //位與1結果位1則為奇數,為0則為偶數
    • 位或
    • 位異或
    • 位非
  • 位掩碼
    位掩碼在電腦科學中是一種常用的技術,可同時判斷多個布林 選項,快速地將數字轉換為布林標誌陣列。掩碼中每個選項的值都等於 2 的冪
var OPTION_A = 1;
var OPTION_B = 2;
var OPTION_C = 4;
var OPTION_D = 8;
var OPTION_E = 16;

通過定義這些選項,你可以用位或操作建立一個數字來包含多個選項:

var options = OPTION_A | OPTION_C | OPTION_D;

可以使用位與操作檢查一個給定的選項是否可用

//is option A in the list?
if (options & OPTION_A) {
  //do something
}
//is option B in the list?
if (options & OPTION_B) {
  //do something
}

像這樣的位掩碼操作非常快,正因為前面提到的原因,操作發生在系統底層。如果許多選項儲存在一起並經常檢查,位掩碼有助於加快整體效能

原生方法

無論你怎樣優化 JavaScript 程式碼,它永遠不會比 JavaScript 引擎提供的原生方法更快。經驗不足的 JavaScript 開發者經常犯的一個錯誤是在程式碼中進行復雜的數學運算,而沒有使用內建 Math 物件中那些效能更好的版本。Math 物件包含專門設計的屬性和方法,使數學運算更容易。

//檢視Math物件所有方法
Object.getOwnPropertyNames(Math);

總結

  • 通過避免使用 eval()和 Function()構造器避免二次評估。此外,給 setTimeout()和 setInterval()傳遞函式引數而不是字串引數。
  • 建立新物件和陣列時使用物件直接量和陣列直接量。它們比非直接量形式建立和初始化更快。
  • 避免重複進行相同工作。當需要檢測瀏覽器時,使用延遲載入或條件預載入
  • 當執行數學遠算時,考慮使用位操作,它直接在數字底層進行操作。
  • 原生方法總是比 JavaScript 寫的東西要快。儘量使用原生方法

九、建立並部署高效能 JavaScript 應用程式

  • 合併 js 檔案,減少 HTTP 請求的數量
  • 以壓縮形式提供 js 檔案(gzip 編碼)
  • 通過設定 HTTP 響應報文頭使 js 檔案可快取,通過向檔名附加時間戳解決快取問題
  • 使用CDN提供 js 檔案,CDN 不僅可以提高效能,它還可以為你管理壓縮和快取

十、工具

當網頁或應用程式變慢時,分析網上傳來的資源,分析指令碼的執行效能,使你能夠集中精力在那些需要努力優化的地方。

  • 使用網路分析器找出載入指令碼和其它頁面資源的瓶頸所在,這有助於決定哪些指令碼需要延遲載入,或者進行進一步分析
  • 儘量延遲載入指令碼以使頁面渲染速度更快,向用戶提供更好的整體體驗。
  • 使用效能分析器找出指令碼執行時速度慢的部分,檢查每個函式所花費的時間,以及函式被呼叫的次數,通過呼叫棧自身提供的一些線索來找出哪些地方應當努力優化

後記

能讀到最後的同學也不容易,畢竟篇幅稍長。本書大概花了三週的零碎時間讀完,建議大家讀一讀。如果大家在看書過程中存在疑問,不妨開啟電腦驗證書中作者的言論,或許會更加深刻。

若文中有錯誤歡迎大家評論指出,或者加我微信好友一起交流gm4118679254