1. 程式人生 > >如何提高AJAX客戶端響應速度

如何提高AJAX客戶端響應速度

如何提高AJAX客戶端響應速度

原文轉載自:http://dorado.group.iteye.com/group/topic/7229

在原文基礎上,本人(以下出現的筆者,均為原作者)做了一些內容修改和格式調整。

AJAX的出現極大地改變了Web應用客戶端的操作模式,它使得使用者可以在全心工作時,不必頻繁的忍受那令人厭惡的頁面重新整理。理論上AJAX技術在很大的程度上可以減少使用者操作的等待時間,同時節約網路上的資料流量。然而,實際情況卻並不總是這樣。使用者時常會抱怨用了AJAX的系統,響應速度反而降低了。

筆者從事AJAX方面的研發多年,參與開發了目前國內較為成熟的AJAX平臺-dorado。根據筆者的經驗,導致這種結果的根本原因並不在AJAX。很多時候系統響應速度的降低都是由不夠合理的介面設計和不夠高效的程式設計習慣造成的。下面我們就來分析幾個AJAX開發過程中需要時刻注意的環節。

1.合理的使用客戶端程式設計和遠端過程呼叫。
  客戶端的程式設計主要都是基於JavaScript的,而JavaScript是一種解釋型的程式語言,它的執行效率相對於Java等都要稍遜一籌,同時JavaScript又是執行在瀏覽器這樣一個嚴格受限的環境當中。因此開發人員對於哪些邏輯可以在客戶端執行應該有一個清醒的認識。
2.儘可能避免頻繁的使用遠端過程呼叫,例如避免在迴圈體中使用遠端過程呼叫。
3.如果可能的話儘可能使用AJAX方式的遠端過程呼叫(非同步方式的遠端過程呼叫)。
4.避免將重量級的資料操作放置在客戶端。例如:大批量的資料複製操作、需要通過大量的資料遍歷完成的計算等。
5.改進對DOM物件的操作方式。
6.提高字串累加的速度
7.避免DOM物件的記憶體洩漏
8.複雜頁面的分段裝載和初始化
9.利用GZIP壓縮網路流量

客戶端的程式設計中,對DOM物件的操作往往是最容易佔用CPU時間的,而且不同的程式設計方法之間的效能差異又往往是非常大的。

以下是三段執行結果完全相同的程式碼,它們的作用是在網頁中建立一個10x1000的表格。然而它們的執行速度卻有著天壤之別。

/* 測試程式碼1 - 耗時: 41秒*/
var table = document.createElement("TABLE");
document.body.appendChild(table);
for(var i = 0; i < 1000; i++){
    var row = table.insertRow(-1);
    for(var j = 0; j < 10; j++){
    var cell = objRow.insertCell(-1);
    cell.innerText = "( " + i + " , " + j + " )";
    }
}

/* 測試程式碼2 - 耗時: 7.6秒 */
var table = document.getElementById("TABLE");
document.body.appendChild(table);
var tbody = document.createElement("TBODY");
table.appendChild(tbody);
for(var i = 0; i < 1000; i++){
var row = document.createElement("TR");
tbody.appendChild(row);
for(var j = 0; j < 10; j++){
    var cell = document.createElement("TD");
    row.appendChild(cell);
    cell.innerText = "( " + i + " , " + j + " )";
}
}

/* 測試程式碼3 - 耗時: 1.26秒 */
var tbody = document.createElement("TBODY");
for(var i = 0; i < 1000; i++){   
  var row = document.createElement("TR");
 for(var j = 0; j < 10; j++){
  var cell = document.createElement("TD");
   cell.innerText = "( " + i + " , " + j + " )";
   row.appendChild(cell);
 }
 tbody.appendChild(row);
}
var table = document.getElementById("TABLE");
table.appendChild(tbody); 
document.body.appendChild(table);

這裡的“測試程式碼1”和“測試程式碼2”之間的差別在於在建立表格單元時使用了不同的API方法,而“測試程式碼2”和“測試程式碼3” 之間的差別在於處理順序的略微不同。

“測試程式碼1”和“測試程式碼2”之間如此大的效能差別我們無從分析,目前所知的是insertRow和insertCell是DHTML中表格特有的API,createElement和appendChild是W3C DOM的原生API。而前者應該是對後者的封裝。不過,我們並不能因此而得出結論認為DOM的原生API總是優於物件特有的API。建議大家在需要頻繁呼叫某一API時,對其效能表現做一些基本的測試。

“測試程式碼2”和“測試程式碼3”之間的效能差異主要來自於他們的構建順序不同。“測試程式碼2”的做法是首先建立最外層的《TABLE》物件,然後再在迴圈中依次建立《TR>和《TD》。而“測試程式碼3”的做法是首先在記憶體中由內到外的構建好整個表格,最後再將它新增到網頁中。這樣做的目的是儘可能的減少瀏覽器重新計算頁面佈局的次數。每當我們將一個物件新增到網頁中時,瀏覽器都會嘗試對頁面中的控制元件的佈局進行重新計算。所以,如果我們能夠首先在記憶體中將整個要構造的物件全部建立好,然後再一次性的新增到網頁中。那麼,瀏覽器將只會做一次佈局的重計算。總結為一句話那就是越晚執行appendChild越好。有時為了提高執行效率,我們甚至可以考慮先使用removeChild將已存在的控制元件從頁面中移除,然後構造完成後再重新將其放置回頁面當中。

對於字串累加的問題

在使用AJAX提交資訊時,我可能常常需要拼裝一些比較大的字串通過XmlHttp來完成POST提交。儘管提交這樣大的資訊的做法看起來並不優雅,但有時我們可能不得不面對這樣的需求。那麼JavaScript中對字串的累加速度如何呢?我們先來做下面的這個實驗。累加一個長度為30000的字串。

/* 測試程式碼1 - 耗時: 14.325秒 */
var str = "";
for (var i = 0; i < 50000; i++) {
  str += "xxxxxx";
}
這段程式碼耗時14.325秒,結果並不理想。現在我們將程式碼改為如下的形式:
/* 測試程式碼2 - 耗時: 0.359秒 */
var str = "";
for (var i = 0; i < 100; i++) {
  var sub = "";
  for (var j = 0; j < 500; j++) {
        sub += "xxxxxx";
 }
 str += sub;
}
這段程式碼耗時0.359秒!同樣的結果,我們做的只是首先拼裝一些較小的字串然後再組裝成更大的字串。這種做法可以有效的在字串拼裝的後期減小記憶體複製的資料量。知道了這一原理之後我們還可以把上面的程式碼進一步拆散以後進行測試。下面的程式碼僅耗時0.140秒。

/* 測試程式碼3 - 耗時: 0.140秒 */
var str = "";    
for (var i1 = 0; i1 < 5; i1++) {
   var str1 = "";
  for (var i2 = 0; i2 < 10; i2++) {
        var str2 = "";
        for (var i3 = 0; i3 < 10; i3++) {
          var str3 = "";
           for (var i4 = 0; i4 < 10; i4++) {
            var str4 = "";
            for (var i5 = 0; i5 < 10; i5++) {
                str4 += "xxxxxx";
            }
            str3 += str4;
        }
        str2 += str3;
    }
    str1 += str2;    
}
str += str1;    
}

不過,上面這種做法也許並不是最好的!如果我們需要提交的資訊是XML格式的(其實絕大多數情況下,我們都可以設法將要提交的資訊組裝成XML格式),我們還能找到更高效更優雅的方法—利用DOM物件為我們組裝字串。下面這段代買組裝一個長度為950015的字串僅須耗時0.890秒。

/* 利用DOM物件組裝資訊 - 耗時: 0.890秒 */
var xmlDoc;    
if (browserType == BROWSER_IE) {
xmlDoc = new ActiveXObject("Msxml.DOMDocument");
}
else {
xmlDoc = document.createElement("DOM");
}
var root = xmlDoc.createElement("root");
for (var i = 0; i < 50000; i++) {
   var node = xmlDoc.createElement("data");
  if (browserType == BROWSER_IE) {
       node.text = "xxxxxx";
  }
  else {
      node.innerText = "xxxxxx";
 }
 root.appendChild(node);
}
xmlDoc.appendChild(root);

var str;
if (browserType == BROWSER_IE) {
   str = xmlDoc.xml;
    }
else {
  str = xmlDoc.innerHTML;
}

對於DOM物件的記憶體洩漏的問題

關於IE中DOM物件的記憶體洩露是一個常常被開發人員忽略的問題,然而它帶來的後果卻是非常嚴重的!它會導致IE的記憶體佔用量持續上升,並且瀏覽器的整體執行速度明顯下降。對於一些洩露比較嚴重的網頁,甚至只要重新整理幾次,執行速度就會降低一倍。

比較常見的記憶體洩漏的模型有“迴圈引用模型”、“閉包函式模型”和“DOM插入順序模型”,對於前兩種洩漏模型,我們都可以通過在網頁析構時解除引用的方式來避免,而對於“DOM插入順序模型”則需要通過改變一些慣有的程式設計習慣的方式來避免。

複雜頁面的分段裝載和初始化

對系統當中某些確實比較複雜而又不便使用IFrame的介面,我們可以對其實施分段裝載。例如對於多頁標籤的介面,我們可以首先下載和初始化多頁標籤的預設頁,然後利用AJAH(asynchronous JavaScript and HTML)技術來非同步的裝載其他標籤頁中的內容。這樣就能保證介面可以在第一時間首先展現給使用者。把整個複雜介面的裝載過程分散到使用者的操作過程當中。

利用GZIP壓縮網路流量

除了上面提到的這些程式碼級的改良之外,我們還可以利用GZIP來有效的降低網路流量。目前常見的主流瀏覽器已經全部支援GZIP演算法,我們往往只需要編寫少量的程式碼就可以支援GZIP了。例如在J2EE中我們可以在Filter中通過下面的程式碼來判斷客戶端瀏覽器是否支援GZIP演算法,然後根據需要利用java.util.zip.GZIPOutputStream來實現GZIP的輸出。

/* 判斷瀏覽器對GZIP支援方式的程式碼 */ 
private static String getGZIPEncoding(HttpServletRequest request) {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding == null) return null;
acceptEncoding = acceptEncoding.toLowerCase();
if (acceptEncoding.indexOf("x-gzip") >= 0) return "x-gzip";
if (acceptEncoding.indexOf("gzip") >= 0) return "gzip";
return null;
}

一般而言,GZIP對於HTML、JSP的壓縮比可以達到80%左右,而它造成的服務端和客戶端的效能損耗幾乎是可以忽略的。結合其他因素,支援GZIP的網站有可能為我們節約50%的網路流量。因此GZIP的使用可以為那些網路環境不是特別好的應用帶來顯著的效能提升。

使用Http的監視工具Fiddler可以方便的檢測出網頁在使用GZIP前後的通訊資料量。Fiddler的下載地址是http://www.fiddlertool.com/fiddler/

關於Web應用的效能優化其實是一個非常大的話題。本文由於篇幅有限,只能涉及其中的幾個細節,並且也無法將這些細節的優化方式全面的展現給大家。期望本文能夠引起大家對Web應用尤其是客戶端效能優化的充分重視,而且在客戶端的方法改進往往能夠得到令人驚奇的效能提升。