教你寫出高性能JavaScript

NO IMAGE

教你寫出高性能 JavaScript

最近又在研究性能優化,於是複習起了三年前自己看完高性能 JavaScript 那本書整理的筆記,覺得雖然三年前整理的東西了,但是現在拿出來還是覺得不過時很有用。本文主要介紹兩大塊頁面級優化代碼級優化;至於實踐與應用還有工具上篇文章有講解過: 前端性能優化及其度量方法
教你寫出高性能JavaScript

頁面級優化

教你寫出高性能JavaScript

頁面級優化-加載和執行

從設計實現層面簡化頁面

  1. 腳本位置:底部
    將腳本內容在頁面信息內容加載後再加載

    將腳本儘可能的往後挪,減少對併發下載的影響。

  2. 組織腳本:減少外鏈

    1. 合理設置 HTTP 緩存
      緩存的力量是強大的,恰當的緩存設置可以大大的減少 HTTP 請求。

      能緩存越多越好,能緩存越久越好。儘可能的讓資源能夠在緩存中待得更久。

      eg.很少變化的圖片資源可以直接通過 HTTP Header 中的 Expires 設置一個很長的過期頭 ;

      變化不頻繁而又可能會變的資源可以使用 Last-Modifed 來做請求驗證

    2. 資源合併與壓縮
      儘可能的將外部的腳本、樣式進行合併,多個合為一個。

      CSS、 Javascript、Image 都可以用相應的工具進行壓縮,壓縮後往往能省下不少空間

      推薦工具: grunt 、gulp、 tool.css-js.com/( UglifyJS )、 webpack

    3. CSS Sprites
      合併 CSS 圖片,減少請求數

    4. Inline Images
      將圖片嵌入到頁面或 CSS 中

    5. Lazy Load Images

      圖片懶加載

  3. 無阻塞腳本:頁面加載完之後再加載 js 代碼
    window 對象的 load 時間觸發之後再下載腳本

    (1)延遲的腳本:並行下載,下載過程中不會產生阻塞
    defer(IE4+和 firefox3.5+)等待頁面完成後執行

    async(HTML5 異步加載腳本)加載完成後自動執行

    eg. github.com/dingxiaoxue…

    (2)動態腳本元素:

    憑藉著它在跨瀏覽器兼容性和易用性的優勢,成為最通用的無阻塞加載解決方案
    eg. github.com/dingxiaoxue…

    注意加載順序

    Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/loadJs.html

    由於這個過程是異步的,因此文件大一點不會有影響

    (3) XMLHttpRequest 腳本注入:無阻塞加載腳本的另一種方法

    優點:同一套代碼適合所有瀏覽器、下載後不會立即執行可推遲到你準備好的時候

    缺點:侷限性 js 文件必須與所有請求的頁面處於同一個域(不適合咱們從 CDN 下載)

    (4)推薦無阻塞模式:

    A.上面的(2) B.YUI3 github.com/yui/yui3/ C.LazyLoad 類庫 D.LABjs

頁面級優化-總結

管理瀏覽器中的 JavaScript 代碼是棘手的問題,因為代碼執行過程會阻塞瀏覽器的其他進程(如用戶界面繪製)每次遇到 script 標籤,頁面都必須停下來等待代碼下載並執行,然後繼續處理其他部分。儘管如此,還是有幾種方法減少 js 對性能的影響:

  1. body 閉合標籤之前,將所有的 script 標籤放在頁面底部。這樣能確保在腳本執行之前頁面已經完成渲染
  2. 合併腳本。頁面中的 script 標籤越少加載的也就越快,響應也越迅速。
  3. 多種無阻塞下載 js 的方法:
    使用
 <script defer>

defer 屬性

使用動態創建的 script 元素來下載並執行代碼

使用 XHR 對象下載 js 代碼並注入頁面

通過以上策略,可以大大提高那些需要大量使用大量 js 的 web 應用的實際性能

代碼級優化

教你寫出高性能JavaScript

代碼級優化-數據存取

  1. 管理作用域:
    (1)作用域鏈和標識符解析

    多次調用同一個函數就會導致創建多個執行環境,函數執行完畢後執行環境被銷燬

    函數調用過程中每個標識符都要經歷一個搜索的過程會影響性能

    (2)標識符解析的性能

    一個標識符的所在位置越深,它的讀寫速度也就越慢;

    在一個沒有優化的 js 引擎瀏覽器中,建議儘可能使用局部變量

    先將一個全局變量的引用存儲在一個局部變量中,然後使用這個局部變量代替全局變量

    eg. github.com/dingxiaoxue…

    (3)改變作用域鏈

    With()

    Try{method()}catch(ex){handleError(ex)//委託給錯誤處理函數}

    Eval()

    儘量避免使用,確實有必要時候再用動態作用域

    (4)閉包、作用域和內存

    閉包比非閉包小號更多的內存,IE 使用非原生的 js 對象來實現 DOM 對象,因此會導致內存洩露

    頻繁訪問跨作用域的標識符時,每次訪問都會帶來性能損失

    跨作用域的處理建議,減輕閉包對執行速度的影響

    將常用的跨作用域變量存儲在局部變量中,然後直接訪問局部變量

  2. 對象成員(屬性和方法)
    (1)原型 prototype

    hasOwnProperty() 判斷對象是否包含特定的實例成員

    In 確定對象是否包含特定的屬性(搜索實例和原型)

    (2)原型鏈 prototype chains

    減少作用域鏈查找

    對象在原型鏈中存在的位置越深,找到他的也就越慢

    (3)嵌套成員
    每次遇見點操作符,嵌套成員會導致 js 搜索引擎搜索所有的對象成員

    對象成員嵌套的越深,讀取速度越慢

    Location.href 最快

    Window.location.href

    window.loacation.href.toString() 最慢

    (4)緩存對象成員值

    不要在同一個函數裡多次查找同一個對象成員,除非他的值改變了

  1. 屬性或方法在原型鏈中位置越深,訪問他的速度也越慢

    字面量:只代表自身,不存儲在特定位置。

    字符串、數字、布爾值、對象、數組、函數、正則表達式、null、undefined

    本地變量:開發人員使用關鍵字 var 定義的數據存儲單元

    數組元素:存儲在 JavaScript 數組對象內部,以數字作為索引

    對象成員:存儲在 JavaScript 對象內部,以字符串作為索引

    不同瀏覽器分別對四種數據存儲位置進行 200000 次操作所用時間

    教你寫出高性能JavaScript

    總結數據存取

    在 JavaScript 中,數據存儲的位置會對代碼整體性能產生重大影響。

    數據存儲共有四種方式:字面量、變量、數組項、對象成員。

    1. 訪問字面量和局部變量的速度最快,相反,訪問數組元素和對象成員相對較慢

    2. 由於局部變量存在於作用域鏈中的其實位置,因此,訪問局部變量比訪問跨作用域變量更快

      變量在作用域中的位置越深,訪問所需要的時間越長。

      由於全局變量總處於作用域的最末端,因此訪問速度也最慢

    3. 避免用 with 語句,因為他會改變執行環境作用域鏈。
      try-catch 語句中的 catch 子句也具有同樣的影響
      因此要小心使用

    4. 嵌套的對象成員會明顯影響性能,儘量少用

    5. 屬性或方法在原型鏈中位置越深,訪問他的速度也越慢

    6. 通常來說,你可以把常用的對象成員、數組元素、跨域變量保存局部變量中來改善 JavaScript 性能,因為局部變量更快
      a. 對任何對象屬性的訪問超過 1 次  

      b. 對任何數組成員的訪問次數超過 1 次

    通過以上策略,可以顯著提升那些需要使用大量變量 JavaScript 的 Web 應用的實際性能

代碼級優化-DOM 編程

代碼級優化-DOM 訪問與修改

DOM 的訪問與修改

最小化 DOM 訪問次數,儘可能在 js 端處理。

如果需要多次訪問某個 DOM 節點,請使用局部變量存儲他的引用。

訪問 DOM 的次數越多,代碼的運行速度越慢

減少訪問 DOM 的次數,把運算儘量留在 ECMAScript 這一端

eg.https://github.com/dingxiaoxue/shareDemo/blob/master/domChange.html(155倍)

教你寫出高性能JavaScript

  1. innerHTML 對比 DOM 方法
    document.creatElement() VS innerHTML

    除了 webkit 內核(chrome 和 Safari)外的所有瀏覽器,innerHTML 會更快一些

    eg. github.com/dingxiaoxue…

    eg. github.com/dingxiaoxue…

    對比使用 innerHTML 和 DOM 方法來創建 1000 行表格;在 IE6 中,innerHTML 要快 3 倍以上,而在新版 webkit 瀏覽器中 innerHTML 則會較慢。教你寫出高性能JavaScript

  2. 節點克隆(稍快一點)
    克隆已有元素——使用 element.cloneNode()替代 document.creatElement()

    Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/cloneDOM.html

  3. HTML 集合
    小心處理 HTML 集合,因為它實時聯繫著底層文檔。把集合的長度緩存到一個變量中,並在迭代中使用它。如果需要經常操作集合,建議把他拷貝到一個數組中。

(1)昂貴的集合
在相同的內容和數量下,遍歷一個數組的速度明顯比遍歷一個 HTML 集合快

優化方法:把集合的長度緩存到一個局部變量中,然後在循環的條件退出語句中使用該變量

Eg. github.com/dingxiaoxue…

toArray() 集合轉數組函數

(2)訪問集合時使用局部變量

在循環中使用局部變量存儲集合引用和集合元素更快

eg. github.com/dingxiaoxue…

教你寫出高性能JavaScript

  1. 遍歷 DOM
    如果可能得話,使用速度更快的 API。

getElementById()    getElementsByTagName()
querySelectorAll()   querySelectorAll() //更快
eg.var elements = document.querySelectorAll('#menu a');
var elements = document.getElementById('menu').getElementsByTagName('a');
var errs = document.querySelectorAll('div.warning, div.notice');

(1)獲取 DOM 元素

childNodes VS nextSilbing (老版本的 IE 中 nextSiblings 更快 7 中 105 倍 6 中 16 倍)

Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/nextSibling.html

(2)元素節點

在所有瀏覽器中,children 都比 childNodes 要快(1.5-3 倍)

IE 中遍歷 children 集合的速度明顯快於遍歷 childNode(IE6 24 倍,IE7 124 倍)

eg.https://github.com/dingxiaoxue/shareDemo/blob/master/variableCom.html

(3)選擇器 API

代碼級優化- 重繪與重排

當瀏覽器下載完所有頁面 HTML 標記,JavaScript,CSS,圖片之後,它解析文件並創建兩個內部數據結構:

一棵樹:表示頁面結構(當佈局和幾何改變時需要重排版)

一棵渲染樹 :表示 DOM 節點如何顯示

1. 重排何時發生

(1)添加或刪除可見的 DOM 元素

(2)元素位置改變

(3)元素尺寸改變(因為邊距,填充,邊框寬度,寬度,高度等屬性改變)

(4)內容改變,例如,文本改變或圖片被另一個不同尺寸的所替代

(5)最初的頁面渲染

(6)瀏覽器窗口改變尺寸

根據改變的性質,渲染樹上或大或小的一部分需要重新計算。某些改變可導致重排版整個頁面:例如,當一個滾動條出現時。

獲取佈局信息的操作將導致刷新隊列動作,最好不要使用以下屬性:

• offsetTop, offsetLeft, offsetWidth, offsetHeight
• scrollTop, scrollLeft, scrollWidth, scrollHeight
• clientTop, clientLeft, clientWidth, clientHeight
• getComputedStyle()(currentStylein IE)(在 IE 中此函數稱為 currentStyle)

2. 最小化重繪和重排

批量改變樣式時,“離線”操作 DOM 樹,使用緩存,並減少訪問佈局信息次數

(1)改變樣式

(2)批量修改 DOM

當你需要對 DOM 元素進行多次修改時,你可以通過以下步驟減少重繪和重排版的次數:

從文檔流中摘除該元素

對其應用多重改變

元素帶回文檔中

三種基本方法可以將 DOM 從文檔中摘除:

隱藏元素,進行修改,然後再顯示它

使用一個文檔片斷在已存 DOM 之外創建一個子樹,然後將它拷貝到文檔中。

將原始元素拷貝到一個脫離文檔的節點中,修改副本,然後覆蓋原始元素。

3.讓元素脫離動畫流

使用絕對座標定位頁面動畫的元素,使它位於頁面佈局流之外。

啟動元素動畫。當它擴大時,它臨時覆蓋部分頁面。這是一個重繪過程,但隻影響頁面的一小部分,避免重排版並重繪一大塊頁面。

當動畫結束時,重新定位,從而只一次下移文檔其他元素的位置。

4.事件委託

使用事件委託來減少事件處理器的數量

5.:hover
少用

代碼級優化- 算法和流程控制

代碼整體結構是影響運行速度的主要因素之一。

代碼的組織結構和解決具體問題的思路是影響代碼性能的主要因素。

  1. 循環
    在大多數變成語言中,代碼執行時間大部分消耗在循環中。

    死循環或長時間運行的循環會嚴重影響用戶體驗。

    循環類型(4 種)

    For(){//循環主體}

    While(){(){//循環主體}

    Do{{//循環主體}while();

    For( var prop in object){//循環主體};

    循環性能

    不斷引發循環性能爭論的源頭是循環類型的選擇。

    Js 中 for-in 循環比其他幾種明顯要慢,另外三種速度區別不大

    迭代一個屬性未知的對象——for-in(不要用 for-in 遍歷數組成員)

    遍歷一個數量有限的已知屬性列表——1/2/3(循環類型的選擇要基於需求,而不是性能)

    A.每次迭代處理事務

    B.迭代次數

    通過減少 A、B 這兩者中的一個或者全部的時間開銷,你就提升循環的整體性能。

    減少迭代的工作量

    限制循環中耗時操作的數量

    有一點需要注意的是,javascript 沒有塊級作用域,只有函數級作用域,也就是說在 for 循環初始化中的 var 語句會創建一個函數級變量而非循環級變量

    優化循環的方法有如下

    1、減少對象成員及數組項的查找次數(使用局部變量保存需要查找的對象成員)

    2、顛倒數組的順序來提高循環性能,也就是從最後一項開始向前處理

     for (var i = arr.length-1; i >= 0 ; i--) {
    //process
    }
    

    3、相信大家都會盡可能的使用 for 循環而非 jQuery 的 each 來遍歷數組,那是因為 jQuery 的 each 方法是基於函數的迭代。儘管基於函數的迭代提供了一個更為便利的迭代方法,但它比基於循環的迭代在慢許多。

    4、有時候我們會想到底是使用 if-else 呢還是使用 switch,事實上在大多數情況下 switch 比 if-else 運行得要快,所以當判斷多於兩個離散值時,switch 語句是更佳的選擇

    5、優化 if-else 最簡單的方法就是確保最可能出現的條件放在首位,另外一個方法就是優化條件判斷的次數,看下面的代碼您就懂了

 if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else if (value == 2) {
return result2;
} else if (value == 3) {
return result3;
} else if (value == 4) {
return result4;
} else if (value == 5) {
return result5;
} else if (value == 6) {
return result6;
} else if (value == 7) {
return result7;
} else if (value == 8) {
return result8;
} else if (value == 9) {
return result9;
} else if (value == 10) {
return result10;
}

下面這種方法就是使用二分搜索法將值域分成一系列區間,然後逐步縮小區範圍,對上面的例子進行的優化

 if (value < 6) {
if (value < 3) {
if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else {
return result2;
}
} else {
if (value == 3) {
return result3;
} else if (value == 4) {
return result4;
} else {
return result5;
}
}
} else {
if (value < 8) {
if (value == 6) {
return result06;
} else if (value == 7) {
return result7;
}
} else {
if (value == 8) {
return result8;
} else if (value == 9) {
return result9;
} else {
return result10;
}
}
}

6、使用遞歸雖然可以把複雜的算法變得簡單,但遞歸函數如果終止條件不明確或缺少終止條件會導致函數長時間運行。所以遞歸函數還可能會遇到瀏覽器“調用棧大小限制”

使用優化後的循環來替代長時間運行的遞歸函數可以提升性能,因為運行一個循環比反覆調用一個函數的開銷要少的多

如果循環資料太多,可以考慮使用如下介紹的達夫設備原理來提升性能

  1. 達夫設備

     var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;
do {
//每次循環最多可調用8次process
switch (startAt) {
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
startAt = 0;
} while (--iterations);
```
2. 
   var i = items.length % 8;
while (i) {
process(items[i--]);
}
i = Math.floor(items.length / 8);
while (i) {
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
}
```
  1. Memoization

    避免重複是 Memoization 的核心思想,它緩存前一次計算結果供後續使用,下面代碼就是利用緩存結果的思想計算階乘的

 function memFactorial(n) {
if (!memFactorial.cache) {
memFactorial.cache = {
"0": 1,
"1": 1
};
}
if (!memFactorial.cache.hasOwnProperty(n)) {
memFactorial.cache[n] = n * memFactorial(n - 1);
}
return memFactorial.cache[n];
}

寫成通用方法如下代碼所示:

function memoize(fundamental, cache) {
cache = cache || {};
var shell = function (arg) {
if (!cache.hasOwnProperty(arg)) {
cache[arg] = fundamental(arg);
}
return cache[arg];
}
return shell;
}
//下面是調用示例
function factorial(n) {
if (n==0) {
return 1;
}else{
return n*factorial(n-1);
}
}
var memfactorial = memoize(factorial, { "0": 1, "1": 1 });
memfactorial(6);
  1. 算法和流程控制小結

    1. 四種循環中(for、for-in、while、do-while),只有 for-in 循環比其它幾種明顯要慢,另外三種速度區別不大
    2. 除非你要迭代一個屬性未知的對象,否則不要使用 for-in 循環
    3. 改善循環性能的最好辦法是減少每次迭代中的運算量,並減少循環迭代的次數
    4. 一般來說,switch 總是比 if-else 更快,但不總是最好的解決方法;當判斷條件較多時,查表法比 if-else 或者 switch 更快
    5. 瀏覽器的調用棧尺寸限制了地櫃算法 JavaScript 中的應用;棧溢出錯誤導致其他代碼也不能正常執行;如果你遇到一個棧溢出錯誤,將方法修改為一個迭代算法或使用製表法可以避免重複工作

代碼級優化- 字符串和正則表達式

str += "one" + "two";
//以下代碼分別用兩行語句直接附加內容給str,從而避免產生臨時字符串 性能比上面提升10%到40%;
str += "one";
str += "two";
//同樣你可以用如下一句達到上面同樣的性能提升
str = str + "one" + "two";
//事實上 str = str + "one" + "two";等價於 str = ((str + "one") + "two");

或許大家都喜歡用 Array.prototype.join 方法將數組中所有元素合併成一個字符串,雖然它是在 IE7 及更早版本瀏覽器 中合併大量字符串唯一高效的途徑,但是事實上在現代大多數瀏覽器中,數組項連接比其它字符串連接的方法更慢。

在大多數情況下,使用 concat 比使用簡單的+和+=要稍慢些。

當連接數量巨大或尺寸巨大的字符串時,數組聯合是連接字符串最慢的方法之一。使用簡單的+和+=取而代之,可避免不必要的中間字符串

代碼級優化- 快速響應用的戶界面

教你寫出高性能JavaScript

代碼級優化- 數據傳輸 Ajax

數據傳輸

有 5 種常用技術用於向服務器請求數據:

  1. XMLHttpRquest(XHR)
  2. Dynamic script tag insertion(動態腳本標籤插入)
  3. iframes
  4. Comet (一種 hack 技術)
  5. Multipart XHR(多部分的 XHR)

在現代高性能 JavaScript 中使用的三種技術是 XHR,動態腳本標籤插入和多部分的 XHR。使用 Comet 和 iframe(作為數據傳輸技術)往往是極限情況,不在這裡討論。

1.XMLHttpRquest:允許異步發送和接收數據,可以在請求中添加任何頭信息和參數,並讀取服務器返回的所有頭信息及響應文本

使用 XHR 時,POST 和 GET 的對比,對於那些不會改變服務器狀態,只會獲取數據(這種稱作冪等行為)的請求,應該使用 GET,經 GET 請求的數據會被緩存起來,如果需要多次請求同一數據的時候它會有助於提升性能 。

只有當請求的 URL 加上參數的長度接近或超過 2048 個字符時,才應該用 POST 獲取數據,這是因為IE 限制 URL 長度,過長時將會導致請求的 URL 截斷

另外需要注意的是:因為響應消息作為腳本標籤的源碼,所以返回的數據必須是可執行的 javascript 代碼,所以你不能使用純 xml,純 json 或其它任何格式的數據,無論哪種格式,都必須封裝在一個回調函數中

使用 XHR 發送數據到服務器時,GET 方式會更快,因為對於少量數據而言,一個 GET 請求往服務器只發送一個數據包,而一個 POST 請求至少發送兩個數據包,一個裝載頭信息,另一個裝載 POST 正文,POST 更適合發送大量數據到服務器

2.Dynamic script tag insertion(動態腳本標籤插入):該技術克服了 XHR 的最大限制:它可以從不同域的服務器上獲取數據。這是一種黑客技術,而不是實例化一個專用對象,你用 JavaScript 創建了一個新腳本標籤,並將它的源屬性設置為一個指向不同域的 URL

var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName_r('head')[0].appendChild(scriptElement);

但是動態腳本標籤插入與 XHR 相比只提供更少的控制。你不能通過請求發送信息頭。參數只能通過 GET 方法傳遞,不能用 POST。你不能設置請求的超時或重試,實際上,你不需要知道它是否失敗了。你必須等待所有數據返回之後才可以訪問它們。你不能訪問響應信息頭或者像訪問字符串那樣訪問整個響應報文。

最後一點非常重要。因為響應報文被用作腳本標籤的源碼,它必須是可執行的 JavaScript。你不能使用裸 XML,或者裸 JSON,任何數據,無論什麼格式,必須在一個回調函數之中被組裝起來。

 var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName_r('head')[0].appendChild(scriptElement);
function jsonCallback(jsonString) {
var data = ('(' + jsonString + ')');
}

在這個例子中,lib.js 文件將調用 jsonCallback 函數組裝數據:

jsonCallback({ "status": 1, "colors": [ "#fff", "#000", "#ff0000" ] });

儘管有這些限制,此技術仍然非常迅速。其響應結果是運行 JavaScript,而不是作為字符串必須被進一步處理。正因為如此,它可能是客戶端上獲取並解析數據最快的方法。我們比較了動態腳本標籤插入和 XHR 的性能,在本章後面 JSON 一節中。

請小心使用這種技術從你不能直接控制的服務器上請求數據。JavaScript 沒有權限或訪問控制的概念,所以你的頁面上任何使用動態腳本標籤插入的代碼都可以完全控制整個頁面。包括修改任何內容、將用戶重定向到另一個站點,或跟蹤他們在頁面上的操作並將數據發送給第三方。使用外部來源的代碼時務必非常小心。

3.Comet 一種 hack 技術:以即時通信為代表的 web 應用程序對數據的 Low Latency 要求,傳統的基於輪詢的方式已經無法滿足,而且也會帶來不好的用戶體驗。於是一種基於 http 長連接的“服務器推”技術便被 hack 出來。這種技術被命名為 Comet,這個術語由 Dojo Toolkit 的項目主管 Alex Russell 在博文 Comet: Low Latency Data for the Browser 首次提出,並沿用下來。

其實,服務器推很早就存在了,在經典的 client/server 模型中有廣泛使用,只是瀏覽器太懶了,並沒有對這種技術提供很好的支持。但是 Ajax 的出現使這種技術在瀏覽器上實現成為可能, google 的 gmail 和 gtalk 的整合首先使用了這種技術。隨著一些關鍵問題的解決(比如 IE 的加載顯示問題),很快這種技術得到了認可,目前已經有很多成熟的開源 Comet 框架。

以下是典型的 Ajax 和 Comet 數據傳輸方式的對比,區別簡單明瞭。典型的 Ajax 通信方式也是 http 協議的經典使用方式,要想取得數據,必須首先發送請求。在 Low Latency 要求比較高的 web 應用中,只能增加服務器請求的頻率。Comet 則不同,客戶端與服務器端保持一個長連接,只有客戶端需要的數據更新時,服務器才主動將數據推送給客戶端。

Comet 的實現主要有兩種方式:

【1】基於 Ajax 的長輪詢(long-polling)方式

瀏覽器發出

【2】基於 Iframe 及 htmlfile 的流(http streaming)方式

Iframe 是 html 標記,這個標記的 src 屬性會保持對指定服務器的長連接請求,服務器端則可以不停地返回數據,相對於第一種方式,這種方式跟傳統的服務器推則更接近。

在第一種方式中,瀏覽器在收到數據後會直接調用 JS 回調函數,但是這種方式該如何響應數據呢?可以通過在返回數據中嵌入 JS 腳本的方式,如“”,服務器端將返回的數據作為回調函數的參數,瀏覽器在收到數據後就會執行這段 JS 腳本。

但是這種方式有一個明顯的不足之處:IE、Morzilla Firefox 下端的進度欄都會顯示加載沒有完成,而且 IE 上方的圖標會不停的轉動,表示加載正在進行。Google 的天才們使用一個稱為“htmlfile”的 ActiveX 解決了在 IE 中的加載顯示問題,並將這種方法應用到了 gmail+gtalk 產品中。

5.Multipart XHR:允許客戶端只用一個 HTTP 請求就可以從服務器向客戶端傳送多個資源,它通過在服務器端將資源打包成一個由雙方約定的字符串分割的長字符串併發送到客戶端,然後用 javaScript 處理那個長字符串,並根據 mime-type 類型和傳入的其它頭信息解析出每個資源

multipart XHR 使用了流的功能,通過監聽 readyState 為 3 的狀態,我們可以在一個較大的響應還沒有完全接受之前就把它分段處理,這樣我們就可以實時處理響應片段,這也是 MXHR 能大幅提升性能的主要原因

使用 Multipart XHR 的缺點(但是它能顯著提升頁面的整體性能):

  1. 獲得的資源不能被瀏覽器緩存
  2. 老版本的 IE 不支持 readyState 為 3 的狀態和 data:URL(圖片不是由 base64 字符串轉換成二進制,而是使用 data:URL 的方式創建,並指定 mime-type 為 image/jpeg 使用 readyState 為 3 是因為你不可能等所有數據都傳輸完成再處理,那樣會很慢)

Beacons 技術

使用 javascript 創建一個新的 Image 對象,並把 src 屬性設置為服務器上腳本的 URL,該 URL 包含我們要通過 GET 傳回的鍵值對數據(並沒有創建 img 元素,也沒有插入 DOM),服務器會接收到數據並保存起來,它需向客戶端發送任何回饋信息。這種方式是給服務器回傳信息最有效的方式,雖然它的優點是性能消耗很小,但它的缺點也顯而易見

發送的數據長度限制得相當小

如果要接收服務器端返回的數據一種方式是監聽 Image 對象的 load 事件,另外一種方式就是檢查服務器返回圖片的寬高來判斷服務器狀態

數據格式

現在 xml 這種數據格式已全然被 json 取代了,原因很多,主要原因是 XML 文件大小太大,解析速度慢,雖然 XPath 在解析 xml 文檔時比 getElementsByTagName 快許多,但 XPath 並未得到廣泛支持

JSON 相對 xml 來說,文件體積相對更少,通用性強

JSON 數據被當成另一個 JavaScript 文件並作為原生代碼執行,為實現這一點,這些數據必須封裝在一個回調函數中,這就是所謂的 JSON 填充(JSON with padding)JSON-P

最快的 JSON 格式就是使用數組形式的 JSON-P

使用 JSON-P 必須注意安全性,因為 JSON-P 必須是可執行的 JavaScript,它可能被任何人調用並使用動態腳本注入技術插入到網站,另一方面,JSON 在 eval 前是無效的 JavaScript,使用 XHR 時它只是被當作字符串獲取,所以不要把任何敏感數據編碼在 JSON-P 中。

理想的數據格式應該是隻包含必要的結構,以便你可以分解出每一個獨立的字段,所以自定義格式相對來說體積更小點,可以快速下載,且易於解析(只要用 split 函數即可),所以當你創建自定義格式時,最重要的決定之一就是採用哪種分隔符

 var rows = req.responseText.split(/\u0001/);//正則表達式作為分隔符
var rows = req.responseText.split("\u0001");//字符串作為分隔符(更為保險)

數據格式總結

總的來說越輕量級的格式越好,最好是 JSON 和字符串分隔的自定義格式。如果數據集很大或者解析時間問題,那麼就使用這兩種格式之一;

JSON_P 數據,用動態腳本標籤插入法獲取。它將數據視為可運行的 JavaScript 而不是字符串,解析速度極快。它也能跨域使用,但不應涉及敏感數據

字符分割的自定義格式,使用 XHR 或動態腳本標籤插入的技術提取,使用 split()解析。此技術解析非常大數據集時比 JSON_P 技術略快,而且通常文件尺寸更小。

緩存數據

  • 在服務器,設置 HTTP 頭信息以確保你的響應會被瀏覽器緩存
  • 在客戶端,把獲取到的信息存儲到本地,從而避免再次請求

如果你希望 Ajax 響應能被瀏覽器緩存,請必須使用 GET 方式發出請求。設置 Expires 頭信息是確保瀏覽器緩存 Ajax 響應最簡單的方法,而且其緩存內容能跨頁面和跨會話

當然也可以手工管理本地緩存,也就是直接把服務器接收到的數據緩存起來

用習慣了 Ajax 類庫了,然後卻連自己怎麼寫一個 XMLHttpRequest 都不知道了,事實上很多 Ajax 類庫都有這樣那樣的侷限(比如說不允許你直接訪問 readystatechange 事件,這也意味著你必須等待完整的響應接收完畢之後才能開始使用它)所以……

編程實踐

教你寫出高性能JavaScript

構建並部署高性能 JavaScript 應用

教你寫出高性能JavaScript

工具

教你寫出高性能JavaScript

本文 demo

github.com/dingxiaoxue…

前端搞起來[1] 前端搞起來[1]掃碼關注xiaoyuanlianer666

小圓臉兒: 公眾號:小圓臉兒(xiaoyuanlianer666)、掘金:小圓臉兒。

相關文章

自定義View實現字母導航控件

Nginx——負載均衡、動靜分離介紹

摸魚小技巧之IDEA調試篇一

Flutter使用Riverpod+Retrofit構建MVVM開發模式