1. 程式人生 > >轉載 -- 前端效能優化指南

轉載 -- 前端效能優化指南

https://segmentfault.com/a/1190000003646305

 

 

前端效能優化指南

AJAX優化


  • 快取AJAX

    • 非同步並不等於即時

  • 請求使用GET

    • 當使用XMLHttpRequest時,而URL長度不到2K,可以使用GET請求資料,GET相比POST更快速。

      • POST型別請求要傳送兩個TCP資料包。

        • 先發送檔案頭。

        • 再發送資料。

      • GET

        型別請求只需要傳送一個TCP資料包。

        • 取決於你的cookie數量。

COOKIE專題


  • 減少COOKIE的大小。

  • 使用無COOKIE的域。

    • 比如圖片CSS等靜態檔案放在靜態資源伺服器上並配置單獨域名,客戶端請求靜態檔案的時候,減少COOKIE反覆傳輸時對主域名的影響。

DOM優化


  • 優化節點修改。

    • 使用cloneNode在外部更新節點然後再通過replace

      與原始節點互換。

      var orig = document.getElementById('container');
      var clone = orig.cloneNode(true);
      var list = ['foo', 'bar', 'baz'];
      var content;
      for (var i = 0; i < list.length; i++) {
      content = document.createTextNode(list[i]);
      clone.appendChild(content);
      }
      orig.parentNode.replaceChild(clone, orig);

  • 優化節點新增

    多個節點插入操作,即使在外面設定節點的元素和風格再插入,由於多個節點還是會引發多次reflow。

    • 優化的方法是建立DocumentFragment,在其中插入節點後再新增到頁面。

      • JQuery中所有的新增節點的操作如append,都是最終呼叫DocumentFragment來實現的,

        createSafeFragment(document) {

        var list = nodeNames.split( "|" ),

        safeFrag = document.createDocumentFragment();
        

        if (safeFrag.createElement) {

        while (list.length) {
            safeFrag.createElement(
                list.pop();
            );
        };

        };
        return safeFrag;

        };
  • 優化CSS樣式轉換。

    如果需要動態更改CSS樣式,儘量採用觸發reflow次數較少的方式。

    • 如以下程式碼逐條更改元素的幾何屬性,理論上會觸發多次reflow

          element.style.fontWeight = 'bold' ;
          element.style.marginLeft= '30px' ;
          element.style.marginRight = '30px' ;
    • 可以通過直接設定元素的className直接設定,只會觸發一次reflow

      element.className = 'selectedAnchor' ;
  • 減少DOM元素數量

    • console中執行命令檢視DOM元素數量。

      `document.getElementsByTagName( '*' ).length`
    • 正常頁面的DOM元素數量一般不應該超過1000

    • DOM元素過多會使DOM元素查詢效率,樣式表匹配效率降低,是頁面效能最主要的瓶頸之一。

  • DOM操作優化。

    • DOM操作效能問題主要有以下原因。

      • DOM元素過多導致元素定位緩慢。

      • 大量的DOM介面呼叫。

        • JAVASCRIPTDOM之間的互動需要通過函式API介面來完成,造成延時,尤其是在迴圈語句中。

      • DOM操作觸發頻繁的reflow(layout)repaint

      • layout發生在repaint之前,所以layout相對來說會造成更多效能損耗。

        • reflow(layout)就是計算頁面元素的幾何資訊。

        • repaint就是繪製頁面元素。

      • DOM進行操作會導致瀏覽器執行迴流reflow

    • 解決方案。

      • JAVASCRIPT執行時間是很短的。

      • 最小化DOM訪問次數,儘可能在js端執行。

      • 如果需要多次訪問某個DOM節點,請使用區域性變數儲存對它的引用。

      • 謹慎處理HTML集合(HTML集合實時連繫底層文件),把集合的長度快取到一個變數中,並在迭代中使用它,如果需要經常操作集合,建議把它拷貝到一個數組中。

      • 如果可能的話,使用速度更快的API,比如querySelectorAllfirstElementChild

      • 要留意重繪和重排。

      • 批量修改樣式時,離線操作DOM樹。

      • 使用快取,並減少訪問佈局的次數。

      • 動畫中使用絕對定位,使用拖放代理。

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

  • 優化DOM互動

    JAVASCRIPT中,DOM操作和互動要消耗大量時間,因為它們往往需要重新渲染整個頁面或者某一個部分。

    • 最小化現場更新

      • 當需要訪問的DOM部分已經已經被渲染為頁面中的一部分,那麼DOM操作和互動的過程就是再進行一次現場更新

        • 現場更新是需要針對現場(相關顯示頁面的部分結構)立即進行更新,每一個更改(不管是插入單個字元還是移除整個片段),都有一個性能損耗。

        • 現場更新進行的越多,程式碼完成執行所花的時間也越長。

    • 多使用innerHTML

      • 有兩種在頁面上建立DOM節點的方法:

        • 使用諸如createElement()appendChild()之類的DOM方法。

        • 使用innerHTML

          • 當使用innerHTML設定為某個值時,後臺會建立一個HTML直譯器,然後使用內部的DOM呼叫來建立DOM結構,而非基於JAVASCRIPTDOM呼叫。由於內部方法是編譯好的而非解釋執行,故執行的更快。

        對於小的DOM更改,兩者效率差不多,但對於大的DOM更改,innerHTML要比標準的DOM方法建立同樣的DOM結構快得多。

  • 迴流reflow

    • 發生場景。

      • 改變窗體大小。

      • 更改字型。

      • 新增移除stylesheet塊。

      • 內容改變哪怕是輸入框輸入文字。

      • CSS虛類被觸發如 :hover。

      • 更改元素的className。

      • 當對DOM節點執行新增或者刪除操作或內容更改時。

      • 動態設定一個style樣式時(比如element.style.width="10px")。

      • 當獲取一個必須經過計算的尺寸值時,比如訪問offsetWidth、clientHeight或者其他需要經過計算的CSS值。

    • 解決問題的關鍵,就是限制通過DOM操作所引發迴流的次數。

      • 在對當前DOM進行操作之前,儘可能多的做一些準備工作,保證N次建立,1次寫入。

      • 在對DOM操作之前,把要操作的元素,先從當前DOM結構中刪除:

        • 通過removeChild()或者replaceChild()實現真正意義上的刪除。

        • 設定該元素的display樣式為“none”。

      • 每次修改元素的style屬性都會觸發迴流操作。

        element.style.backgroundColor = "blue";

        • 使用更改className的方式替換style.xxx=xxx的方式。

        • 使用style.cssText = '';一次寫入樣式。

        • 避免設定過多的行內樣式。

        • 新增的結構外元素儘量設定它們的位置為fixedabsolute

        • 避免使用表格來佈局。

        • 避免在CSS中使用JavaScript expressions(IE only)

      • 將獲取的DOM資料快取起來。這種方法,對獲取那些會觸發迴流操作的屬性(比如offsetWidth等)尤為重要。

      • 當對HTMLCollection物件進行操作時,應該將訪問的次數儘可能的降至最低,最簡單的,你可以將length屬性快取在一個本地變數中,這樣就能大幅度的提高迴圈的效率。

eval優化


  • 避免eval

    • eval會在時間方面帶來一些效率,但也有很多缺點。

      • eval會導致程式碼看起來更髒。

      • eval會需要消耗大量時間。

      • eval會逃過大多數壓縮工具的壓縮。

HTML優化


  • 插入HTML

    • JavaScript中使用document.write生成頁面內容會效率較低,可以找一個容器元素,比如指定一個div,並使用innerHTML來將HTML程式碼插入到頁面中。

  • 避免空的srchref

    • link標籤的href屬性為空、script標籤的src屬性為空的時候,瀏覽器渲染的時候會把當前頁面的URL作為它們的屬性值,從而把頁面的內容載入進來作為它們的值。

  • 為檔案頭指定Expires

    • 使內容具有快取性,避免了接下來的頁面訪問中不必要的HTTP請求。

  • 重構HTML,把重要內容的優先順序提高。

  • Post-load(次要載入)不是必須的資源。

  • 利用預載入優化資源。

  • 合理架構,使DOM結構儘量簡單。

  • 利用LocalStorage合理快取資源。

  • 儘量避免CSS表示式和濾鏡。

  • 嘗試使用defer方式載入Js指令碼。

  • 新特性:will-change,把即將發生的改變預先告訴瀏覽器。

  • 新特性Beacon,不堵塞佇列的非同步資料傳送。

  • 不同之處:網路緩慢,快取更小,不令人滿意的瀏覽器處理機制。

  • 儘量多地快取檔案。

  • 使用HTML5 Web Workers來允許多執行緒工作。

  • 為不同的Viewports設定不同大小的Content。

  • 正確設定可Tap的目標的大小。

  • 使用響應式圖片。

  • 支援新介面協議(如HTTP2)。

  • 未來的快取離線機制:Service Workers。

  • 未來的資源優化Resource Hints(preconnect, preload, 和prerender)。

  • 使用Server-sent Events。

  • 設定一個Meta Viewport。

JITGC優化


  • untyped(無型別)。

    • JAVASCRIPT是個無型別的語言,這導致瞭如x=y+z這種表示式可以有很多含義。

      • yz是數字,則+表示加法。

      • yz是字串,則+表示字串連線。

      而JS引擎內部則使用“細粒度”的型別,比如:

      • 32-bit* integer。

      • 64-bit* floating-point。

      這就要求js型別-js引擎型別,需要做“boxed/unboxed(裝箱/解箱)”,在處理一次x=y+z這種計算,需要經過的步驟如下。

      1. 從記憶體,讀取x=y+z的操作符。

      2. 從記憶體,讀取yz

      3. 檢查y,z型別,確定操作的行為。

      4. unbox y,z

      5. 執行操作符的行為。

      6. box x

      7. x寫入記憶體。

      只有第5步驟是真正有效的操作,其他步驟都是為第5步驟做準備/收尾,JAVASCRIPTuntyped特性很好用,但也為此付出了很大的效能代價。

  • JIT

    • 先看看JITuntyped的優化,在JIT下,執行x=y+z流程。

      1. 從記憶體,讀取x=y+z的操作符。

      2. 從記憶體,讀取 yz

      3. 檢查yz型別,確定操作的行為。

      4. unbox y,z

      5. 執行 操作符 的行為。

      6. box x

      7. x寫入記憶體。

      其中12步驟由CPU負責,7步驟JIT把結果儲存在暫存器裡。但可惜不是所有情況都能使用JIT,當number+numberstring+string 等等可以使用JIT,但特殊情況,如:number+undefined就不行了,只能走舊解析器。

    • 新引擎還對“物件屬性”訪問做了優化,解決方案叫inline caching,簡稱:IC。簡單的說,就是做cache。但如果當list很大時,這種方案反而影響效率。

  • Type-specializing JIT

    Type-specializing JIT引擎用來處理typed型別(宣告型別)變數,但JAVASCRIPT都是untype型別的。

    • Type-specializing JIT的解決方案是:

      • 先通過掃描,監測型別。

      • 通過編譯優化(優化物件不僅僅只是“型別”,還包括對JS程式碼的優化,但核心是型別優化),生成型別變數。

      • 再做後續計算。

    • Type-specializing JIT的執行x=y+z流程:

      • 從記憶體,讀取x=y+z的操作符。

      • 從記憶體,讀取yz

      • 檢查yz型別,確定操作的行為。

      • unbox y,z

      • 執行操作符的行為。

      • box x

      • x寫入記憶體。

    代價是:

    • 前置的掃描型別

    • 編譯優化。

    所以·Type-specializing JIT·的應用是有選擇性,選擇使用這個引擎的場景包括:

    • 熱點程式碼。

    • 通過啟發式演算法估算出來的有價值的程式碼。

    另外,有2點也需要注意:

    • 當變數型別 發生變化時,引擎有2種處理方式:

      • 少量變更,重編譯,再執行。

      • 大量變更,交給JIT執行。

    • 陣列object properties, 閉包變數 不在優化範疇之列。

js載入優化


  • 加快JavaScript裝入速度的工具:

    • Lab.js

      • 藉助LAB.js(裝入和阻止JavaScript),你就可以並行裝入JavaScript檔案,加快總的裝入過程。此外,你還可以為需要裝入的指令碼設定某個順序,那樣就能確保依賴關係的完整性。此外,開發者聲稱其網站上的速度提升了2倍。

  • 使用適當的CDN:

    • 現在許多網頁使用內容分發網路(CDN)。它可以改進你的快取機制,因為每個人都可以使用它。它還能為你節省一些頻寬。你很容易使用ping檢測或使用Firebug除錯那些伺服器,以便搞清可以從哪些方面加快資料的速度。選擇CDN時,要照顧到你網站那些訪客的位置。記得儘可能使用公共儲存庫。

  • 網頁末尾裝入JavaScript:

    • 也可以在頭部分放置需要裝入的一些JavaScript,但是前提是它以非同步方式裝入。

  • 非同步裝入跟蹤程式碼:

    指令碼載入與解析會阻塞HTML渲染,可以通過非同步載入方式來避免渲染阻塞,步載入的方式很多,比較通用的方法如下。

    var _gaq = _gaq || []; 
        _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']); 
        _gaq.push(['_trackPageview']); 
    (function() { 
        var ga = document.createElement('script'); ga.type = 'text/JavaScript'; ga.async = true; 
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 
    })();
    

或者

    function loadjs (script_filename){
         var script = document.createElement( 'script' );
         script.setAttribute( 'type' , 'text/javascript' );
         script.setAttribute( 'src' , script_filename);
         script.setAttribute( 'id' , 'script-id' );

         scriptElement = document.getElementById( 'script-id' );
         if (scriptElement){
             document.getElementsByTagName( 'head' )[0].removeChild(scriptElement);
         }
         document.getElementsByTagName( 'head' )[0].appendChild(script);
    }
    var script = 'scripts/alert.js' ;
    loadjs(script);
  • 把你的JavaScript打包成PNG檔案

    • 將JavaScript/css資料打包成PNG檔案。之後進行拆包,只要使用畫布API的getImageData()。可以在不縮小資料的情況下,多壓縮35%左右。而且是無失真壓縮,對比較龐大的指令碼來說,在圖片指向畫布、讀取畫素的過程中,你會覺得有“一段”裝入時間。

  • 設定Cache-Control和Expires頭

    通過Cache-Control和Expires頭可以將指令碼檔案快取在客戶端或者代理伺服器上,可以減少指令碼下載的時間。

    Expires格式:

    Expires = "Expires" ":" HTTP-date
    Expires: Thu, 01 Dec 1994 16:00:00 GMT
    Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the
    Expires field.
    

    Cache-Control格式:

    Cache-Control   = "Cache-Control" ":" 1#cache-directive
    Cache-Control: public
    

具體的標準定義可以參考http1.1中的定義,簡單來說Expires控制過期時間是多久,Cache-Control控制什麼地方可以快取 。

with優化


  • 儘可能地少用with語句,因為它會增加with語句以外的資料的訪問代價。

  • 避免使用with

    >
    `with`語句將一個新的可變物件推入作用域鏈的頭部,函式的所有區域性變數現在處於第二個作用域鏈物件中,從而使區域性變數的訪問代價提高。
    

    var person = {

     name: “Nicholas",
     age: 30

    }
    function displayInfo() {

     var count = 5;
     with (person) {
         alert(name + ' is ' + age);
         alert( 'count is ' + count);
     }

    }

變數專題


  • 全域性變數

    • 當一個變數被定義在全域性作用域中,預設情況下JAVASCRIPT引擎就不會將其回收銷燬。如此該變數就會一直存在於老生代堆記憶體中,直到頁面被關閉。

    • 全域性變數缺點。

      • 使變數不易被回收。

        • 多人協作時容易產生混淆。

      • 在作用域鏈中容易被幹擾。

    • 可以通過包裝函式來處理全域性變數

  • 區域性變數。

    • 儘量選用區域性變數而不是全域性變數。

    • 區域性變數的訪問速度要比全域性變數的訪問速度更快,因為全域性變數其實是window物件的成員,而區域性變數是放在函式的棧裡的。

  • 手工解除變數引用

    • 在業務程式碼中,一個變數已經確定不再需要了,那麼就可以手工解除變數引用,以使其被回收。

      var data = { / some big data / };
      // ...
      data = null;

  • 變數查詢優化。

    • 變數宣告帶上var,如果宣告變數忘記了var,那麼JAVASCRIPT引擎將會遍歷整個作用域查詢這個變數,結果不管找到與否,都會造成效能損耗。

      • 如果在上級作用域找到了這個變數,上級作用域變數的內容將被無聲的改寫,導致莫名奇妙的錯誤發生。

      • 如果在上級作用域沒有找到該變數,這個變數將自動被宣告為全域性變數,然而卻都找不到這個全域性變數的定義。

    • 慎用全域性變數。

      • 全域性變數需要搜尋更長的作用域鏈。

      • 全域性變數的生命週期比區域性變數長,不利於記憶體釋放。

      • 過多的全域性變數容易造成混淆,增大產生bug的可能性。

    • 具有相同作用域變數通過一個var宣告。

      jQuery.extend = jQuery.fn.extend = function () {
          var options, 
              name, 
              src, 
              copy, 
              copyIsArray, 
              clone,target = arguments[0] || {},
              i = 1,
              length = arguments.length,
              deep = false ;
      }
    • 快取重複使用的全域性變數。

      • 全域性變數要比區域性變數需要搜尋的作用域長

      • 重複呼叫的方法也可以通過區域性快取來提速

      • 該項優化在IE上體現比較明顯

        var docElem = window.document.documentElement,

        selector_hasDuplicate,
        matches = docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector ||docElem.msMatchesSelector,
        selector_sortOrder = function ( a, b ) {
            // Flag for duplicate removal
            if ( a === b ) {
                 selector_hasDuplicate = true ;
                 return 0;
            }
        }
  • 善用回撥。

    • 除了使用閉包進行內部變數訪問,我們還可以使用現在十分流行的回撥函式來進行業務處理。

          function getData(callback) {
            var data = 'some big data';
            callback(null, data);
          }
          getData(function(err, data) {
            console.log(data);
          });
      • 回撥函式是一種後續傳遞風格(Continuation Passing StyleCPS)的技術,這種風格的程式編寫將函式的業務重點從返回值轉移到回撥函式中去。而且其相比閉包的好處也有很多。

        • 如果傳入的引數是基礎型別(如字串、數值),回撥函式中傳入的形參就會是複製值,業務程式碼使用完畢以後,更容易被回收。

        • 通過回撥,我們除了可以完成同步的請求外,還可以用在非同步程式設計中,這也就是現在非常流行的一種編寫風格。

        • 回撥函式自身通常也是臨時的匿名函式,一旦請求函式執行完畢,回撥函式自身的引用就會被解除,自身也得到回收。

常規優化


  • 傳遞方法取代方法字串

    一些方法例如setTimeout()setInterval(),接受字串或者方法例項作為引數。直接傳遞方法物件作為引數來避免對字串的二次解析。

    • 傳遞方法

         setTimeout(test, 1);
      
    • 傳遞方法字串

      setTimeout('test()', 1);
  • 使用原始操作代替方法呼叫

    方法呼叫一般封裝了原始操作,在效能要求高的邏輯中,可以使用原始操作代替方法呼叫來提高效能。

    • 原始操作

      var min = a<b?a:b;

    • 方法例項

      var min = Math.min(a, b);

  • 定時器

    如果針對的是不斷執行的程式碼,不應該使用setTimeout,而應該是用setIntervalsetTimeout每次要重新設定一個定時器。

  • 避免雙重解釋

JAVASCRIPT程式碼想解析JAVASCRIPT程式碼時就會存在雙重解釋懲罰,雙重解釋一般在使用eval函式、new Function建構函式和setTimeout傳一個字串時等情況下會遇到,如。

    eval("alert('hello world');");
    var sayHi = new Function("alert('hello world');");
    setTimeout("alert('hello world');", 100);

上述alert('hello world');語句包含在字串中,即在JS程式碼執行的同時必須新啟運一個解析器來解析新的程式碼,而例項化一個新的解析器有很大的效能損耗。

   我們看看下面的例子:

    var sum, num1 = 1, num2 = 2;
    /**效率低**/
    for(var i = 0; i < 10000; i++){
        var func = new Function("sum+=num1;num1+=num2;num2++;");
        func();
        //eval("sum+=num1;num1+=num2;num2++;");
    }
    /**效率高**/
    for(var i = 0; i < 10000; i++){
        sum+=num1;
        num1+=num2;
        num2++;
    }

第一種情況我們是使用了new Function來進行雙重解釋,而第二種是避免了雙重解釋。

  • 原生方法更快

    • 只要有可能,使用原生方法而不是自已用JS重寫。原生方法是用諸如C/C++之類的編譯型語言寫出來的,要比JS的快多了。

  • 最小化語句數

    JS程式碼中的語句數量也會影響所執行的操作的速度,完成多個操作的單個語句要比完成單個操作的多個語句塊快。故要找出可以組合在一起的語句,以減來整體的執行時間。這裡列舉幾種模式

    • 多個變數宣告

      /不提倡/
      var i = 1;
      var j = "hello";
      var arr = [1,2,3];
      var now = new Date();
      /提倡/
      var i = 1,

      j = "hello",
      arr = [1,2,3],
      now = new Date();
      
    • 插入迭代值

      /不提倡/
      var name = values[i];
      i++;
      /提倡/
      var name = values[i++];

    • 使用陣列和物件字面量,避免使用建構函式Array(),Object()

      /不提倡/
      var a = new Array();
      a[0] = 1;
      a[1] = "hello";
      a[2] = 45;
      var o = new Obejct();
      o.name = "bill";
      o.age = 13;
      /提倡/
      var a = [1, "hello", 45];
      var o = {

      name : "bill",
      age : 13

      };

  • 避免使用屬性訪問方法

    • JavaScript不需要屬性訪問方法,因為所有的屬性都是外部可見的。

    • 新增屬性訪問方法只是增加了一層重定向 ,對於訪問控制沒有意義。

      使用屬性訪問方法示例
      

      function Car() {
      this .m_tireSize = 17;
      this .m_maxSpeed = 250;
      this .GetTireSize = Car_get_tireSize;
      this .SetTireSize = Car_put_tireSize;
      }

      function Car_get_tireSize() {
      return this .m_tireSize;
      }

      function Car_put_tireSize(value) {
      this .m_tireSize = value;
      }
      var ooCar = new Car();
      var iTireSize = ooCar.GetTireSize();
      ooCar.SetTireSize(iTireSize + 1);

      直接訪問屬性示例
      

      function Car() {
      this .m_tireSize = 17;
      this .m_maxSpeed = 250;
      }
      var perfCar = new Car();
      var iTireSize = perfCar.m_tireSize;
      perfCar.m_tireSize = iTireSize + 1;

  • 減少使用元素位置操作

    • 一般瀏覽器都會使用增量reflow的方式將需要reflow的操作積累到一定程度然後再一起觸發,但是如果指令碼中要獲取以下屬性,那麼積累的reflow將會馬上執行,已得到準確的位置資訊。

           offsetLeft
           offsetTop
           offsetHeight
           offsetWidth
           scrollTop/Left/Width/Height
           clientTop/Left/Width/Height
           getComputedStyle()

程式碼壓縮


  • 程式碼壓縮工具

    精簡程式碼就是將程式碼中的空格註釋去除,也有更進一步的會對變數名稱混淆精簡。根據統計精簡後文件大小會平均減少21%,即使Gzip之後檔案也會減少5%

    • YUICompressor

    • Dean Edwards Packer

    • JSMin

    • GZip壓縮

      • GZip縮短在瀏覽器和伺服器之間傳送資料的時間,縮短時間後得到標題是Accept-Encodinggzip,deflate的一個檔案。不過這種壓縮方法同樣也有缺點。

        • 它在伺服器端和客戶端都要佔用處理器資源(以便壓縮和解壓縮)。

        • 佔用磁碟空間。

      • Gzip通常可以減少70%網頁內容的大小,包括指令碼、樣式表、圖片等任何一個文字型別的響應,包括XMLJSONGzipdeflate更高效,主流伺服器都有相應的壓縮支援模組。

      • Gzip的工作流程為

        • 客戶端在請求Accept-Encoding中宣告可以支援Gzip

        • 伺服器將請求文件壓縮,並在Content-Encoding中宣告該回復為Gzip格式。

        • 客戶端收到之後按照Gzip解壓縮。

    • Closure compiler

程式碼優化


  • 優化原則:

    JS與其他語言不同在於它的執行效率很大程度是取決於JS engine的效率。除了引擎實現的優劣外,引擎自己也會為一些特殊的程式碼模式採取一些優化的策略。例如FFOperaSafariJAVASCRIPT引擎,都對字串的拼接運算(+)做了特別優化。所以應該根據不同引擎進行不同優化。

    而如果做跨瀏覽器的web程式設計,則最大的問題是在於IE6(JScript 5.6),因為在不打hotfix的情況下,JScript引擎的垃圾回收的bug,會導致其在真實應用中的performance跟其他瀏覽器根本不在一個數量級上。因此在這種場合做優化,實際上就是為JScript做優化,所以第一原則就是隻需要為IE6(未打補丁的JScript 5.6或更早版本)做優化。

    • JS優化總是出現在大規模迴圈的地方:

       這倒不是說迴圈本身有效能問題,而是迴圈會迅速放大可能存在的效能問題,所以第二原則就是以大規模迴圈體為最主要優化物件。
      

    以下的優化原則,只在大規模迴圈中才有意義,在迴圈體之外做此類優化基本上是沒有意義的。

    目前絕大多數JS引擎都是解釋執行的,而解釋執行的情況下,在所有操作中,函式呼叫的效率是較低的。此外,過深的prototype繼承鏈或者多級引用也會降低效率。JScript中,10級引用的開銷大體是一次空函式呼叫開銷的1/2。這兩者的開銷都遠遠大於簡單操作(如四則運算)。

    • 儘量避免過多的引用層級和不必要的多次方法呼叫:

      特別要注意的是,有些情況下看似是屬性訪問,實際上是方法呼叫。例如所有DOM的屬性,實際上都是方法。在遍歷一個NodeList的時候,迴圈 條件對於nodes.length的訪問,看似屬性讀取,實際上是等價於函式呼叫的。而且IE DOM的實現上,childNodes.length每次是要通過內部遍歷重新計數的。(My god,但是這是真的!因為我測過,childNodes.length的訪問時間與childNodes.length的值成正比!)這非常耗費。所以 預先把nodes.length儲存到js變數,當然可以提高遍歷的效能。
      

    同樣是函式呼叫,使用者自定義函式的效率又遠遠低於語言內建函式,因為後者是對引擎本地方法的包裝,而引擎通常是c,c++,java寫的。進一步,同樣的功能,語言內建構造的開銷通常又比內建函式呼叫要效率高,因為前者在JS程式碼的parse階段就可以確定和優化。

    • 儘量使用語言本身的構造和內建函式:

      這裡有一個例子是高效能的String.format方法。 String.format傳統的實現方式是用String.replace(regex, func),在pattern包含n個佔位符(包括重複的)時,自定義函式func就被呼叫n次。而這個高效能實現中,每次format呼叫所作的只是一次Array.join然後一次String.replace(regex, string)的操作,兩者都是引擎內建方法,而不會有任何自定義函式呼叫。兩次內建方法呼叫和n次的自定義方法呼叫,這就是效能上的差別。
      

    同樣是內建特性,效能上也還是有差別的。例如在JScript中對於arguments的訪問效能就很差,幾乎趕上一次函式呼叫了。因此如果一個 可變引數的簡單函式成為效能瓶頸的時候,可以將其內部做一些改變,不要訪問arguments,而是通過對引數的顯式判斷來處理,比如:

動畫優化


  • 動畫效果在缺少硬體加速支援的情況下反應緩慢,例如手機客戶端。

    • 特效應該只在確實能改善使用者體驗時才使用,而不應用於炫耀或者彌補功能與可用性上的缺陷。

    • 至少要給使用者一個選擇可以禁用動畫效果。

    • 設定動畫元素為absolute或fixed。

      • position: staticposition: relative元素應用動畫效果會造成頻繁的reflow

      • position: absoluteposition: fixed的元素應用動畫效果只需要repaint

    • 使用一個timer完成多個元素動畫。

      • setIntervalsetTimeout是兩個常用的實現動畫的介面,用以間隔更新元素的風格與佈局。。

    • 動畫效果的幀率最優化的情況是使用一個timer完成多個物件的動畫效果,其原因在於多個timer的呼叫本身就會損耗一定效能。

      setInterval(function() {
        animateFirst('');
      }, 10);
      setInterval(function() {
        animateSecond('');
      }, 10);
      

    使用同一個timer

        setInterval(function() {
          animateFirst('');
          animateSecond('');
        }, 10);
  • 以指令碼為基礎的動畫,由瀏覽器控制動畫的更新頻率。

物件專題


  • 減少不必要的物件建立:

    • 建立物件本身對效能影響並不大,但由於JAVASCRIPT的垃圾回收排程演算法,導致隨著物件個數的增加,效能會開始嚴重下降(複雜度O(n^2))。

      • 如常見的字串拼接問題,單純的多次建立字串物件其實根本不是降低效能的主要原因,而是是在物件建立期間的無謂的垃圾回收的開銷。而Array.join的方式,不會建立中間字串物件,因此就減少了垃圾回收的開銷。

    • 複雜的JAVASCRIPT物件,其建立時時間和空間的開銷都很大,應該儘量考慮採用快取。

    • 儘量作用JSON格式來建立物件,而不是var obj=new Object()方法。前者是直接複製,而後者需要呼叫構造器。

  • 物件查詢

    • 避免物件的巢狀查詢,因為JAVASCRIPT的解釋性,a.b.c.d.e巢狀物件,需要進行4次查詢,巢狀的物件成員會明顯影響效能。

    • 如果出現巢狀物件,可以利用區域性變數,把它放入一個臨時的地方進行查詢。

  • 物件屬性

    • 訪問物件屬性消耗效能過程(JAVASCRIPT物件儲存)。

      • 先從本地變量表找到物件

      • 然後遍歷屬性

      • 如果在當前物件屬性列表裡沒找到。

      • 繼續從prototype向上查詢。

      • 且不能直接索引,只能遍歷。

        function f(obj) {

        return obj.a + 1;

        }

服務端優化


  • 避免404。

    • 更改404錯誤響應頁面可以改進使用者體驗,但是同樣也會浪費伺服器資源。

    • 指向外部JAVASCRIPT的連結出現問題並返回404程式碼。

      • 這種載入會破壞並行載入。

      • 其次瀏覽器會把試圖在返回的404響應內容中找到可能有用的部分當作JavaScript程式碼來執行。

  • 刪除重複的JAVASCRIPTCSS

    • 重複呼叫指令碼缺點。

      • 增加額外的HTTP請求。

      • 多次運算也會浪費時間。在IE和Firefox中不管指令碼是否可快取,它們都存在重複運算JAVASCRIPT的問題。

  • ETags配置Entity標籤。

    • ETags用來判斷瀏覽器快取裡的元素是否和原來伺服器上的一致。

      • last-modified date相比更靈活。

        >如某個檔案在1秒內修改了10次,`ETags`可以綜合`Inode`(檔案的索引節點`inode`數),`MTime`(修改時間)和`Size`來精準的進行判斷,避開`UNIX`記錄`MTime`只能精確到秒的問題。伺服器叢集使用,可取後兩個引數。使用`ETags`減少`Web`應用頻寬和負載
  • 權衡DNS查詢次數

    • 減少主機名可以節省響應時間。但同時也會減少頁面中並行下載的數量。

      • IE瀏覽器在同一時刻只能從同一域名下載兩個檔案。當在一個頁面顯示多張圖片時,IE使用者的圖片下載速度就會受到影響。

  • 通過Keep-alive機制減少TCP連線。

  • 通過CDN減少延時。

  • 平行處理請求(參考BigPipe)。

  • 通過合併檔案或者Image Sprites減少HTTP請求。

  • 減少重定向( HTTP 301和40x/50x)。

型別轉換專題


  • 把數字轉換成字串。

    • 應用""+1,效率是最高。

      • 效能上來說:""+字串>String()>.toString()>new String()

        • String()屬於內部函式,所以速度很快。

        • .toString()要查詢原型中的函式,所以速度略慢。

        • new String()最慢。

  • 浮點數轉換成整型。

    • 錯誤使用使用parseInt()

      • parseInt()是用於將字串轉換成數字,而不是浮點數整型之間的轉換。

    • 應該使用Math.floor()或者Math.round()

      • Math是內部物件,所以Math.floor()其實並沒有多少查詢方法和呼叫的時間,速度是最快的。

邏輯判斷優化


  • switch語句。

    • 若有一系列複雜的if-else語句,可以轉換成單個switch語句則可以得到更快的程式碼,還可以通過將case語句按照最可能的到最不可能的順序進行組織,來進一步優化。

記憶體專題


  • JAVASCRIPT的記憶體回收機制

    • 以Google的V8引擎為例,在V8引擎中所有的JAVASCRIPT物件都是通過來進行記憶體分配的。當我們在程式碼中宣告變數賦值時,V8引擎就會在堆記憶體中分配一部分給這個變數。如果已申請的記憶體不足以儲存這個變數時,V8引擎就會繼續申請記憶體,直到的大小達到了V8引擎的記憶體上限為止(預設情況下,V8引擎的堆記憶體的大小上限在64位系統中為1464MB,在32位系統中則為732MB)。

    • 另外,V8引擎對堆記憶體中的JAVASCRIPT物件進行分代管理

      • 新生代。

        • 新生代即存活週期較短的JAVASCRIPT物件,如臨時變數、字串等

      • 老生代。

        • 老生代則為經過多次垃圾回收仍然存活,存活週期較長的物件,如主控制器、伺服器物件等。

  • 垃圾回收演算法。

    • 垃圾回收演算法一直是程式語言的研發中是否重要的​​一環,而V8引擎所使用的垃圾回收演算法主要有以下幾種。

      • Scavange演算法:通過複製的方式進行記憶體空間管理,主要用於新生代的記憶體空間;

      • Mark-Sweep演算法和Mark-Compact演算法:通過標記來對堆記憶體進行整理和回收,主要用於老生代物件的檢查和回收。

  • 物件進行回收。

    • 引用

      • 當函式執行完畢時,在函式內部所宣告的物件不一定就會被銷燬。

      • 引用(Reference)是JAVASCRIPT程式設計中十分重要的一個機制。

        • 是指程式碼對物件的訪問這一抽象關係,它與C/C++的指標有點相似,但並非同物。引用同時也是JAVASCRIPT引擎在進行垃圾回收中最關鍵的一個機制。

          var val = 'hello world';
          function foo() {
          return function() {

          return val;

          };
          }
          global.bar = foo();

        • 當代碼執行完畢時,物件valbar()並沒有被回收釋放,JAVASCRIPT程式碼中,每個變數作為單獨一行而不做任何操作,JAVASCRIPT引擎都會認為這是對物件的訪問行為,存在了對物件的引用。為了保證垃圾回收的行為不影響程式邏輯的執行,JAVASCRIPT引擎不會把正在使用的物件進行回收。所以判斷物件是否正在使用中的標準,就是是否仍然存在對該物件引用

          • JAVASCRIPT引用是可以進行轉移的,那麼就有可能出現某些引用被帶到了全域性作用域,但事實上在業務邏輯裡已經不需要對其進行訪問了,這個時候就應該被回收,但是JAVASCRIPT引擎仍會認為程式仍然需要它。

  • IE下閉包引起跨頁面記憶體洩露。

  • JAVASCRIPT的記憶體洩露處理

    • DOM物件新增的屬性是一個物件的引用。

      var MyObject = {};
      document.getElementByIdx_x('myDiv').myProp = MyObject;

      解決方法:在window.onunload事件中寫上: 
      

      document.getElementByIdx_x('myDiv').myProp = null;

    • DOM物件與JS物件相互引用。

      function Encapsulator(element) {

      this.elementReference = element;
      element.myProp = this;

      }
      new Encapsulator(document.getElementByIdx_x('myDiv'));

      解決方法:在onunload事件中寫上: 
      

      document.getElementByIdx_x('myDiv').myProp = null;

    • 給DOM物件用attachEvent繫結事件。

      function doClick() {}
      element.attachEvent("onclick", doClick);

      解決方法:在onunload事件中寫上: 
      

      element.detachEvent('onclick', doClick);

    • 從外到內執行appendChild。這時即使呼叫removeChild也無法釋放。

      var parentDiv = document.createElement_x("div");
      var childDiv = document.createElement_x("div");
      document.body.appendChild(parentDiv);
      parentDiv.appendChild(childDiv);

      解決方法:從內到外執行appendChild:
      

      var parentDiv = document.createElement_x("div");
      var childDiv = document.createElement_x("div");
      parentDiv.appendChild(childDiv);
      document.body.appendChild(parentDiv);

    • 反覆重寫同一個屬性會造成記憶體大量佔用(但關閉IE後記憶體會被釋放)。

      for(i = 0; i < 5000; i++) {

      hostElement.text = "asdfasdfasdf";

      }

      這種方式相當於定義了5000個屬性,解決方法:無。
  • 記憶體不是快取

    • 不要輕易將記憶體當作快取使用。

    • 如果是很重要的資源,請不要直接放在記憶體中,或者制定過期機制,自動銷燬過期快取

  • CollectGarbage

    • CollectGarbageIE的一個特有屬性,用於釋放記憶體的使用方法,將該變數或引用物件設定為nulldelete然後在進行釋放動作,在做CollectGarbage前,要必需清楚的兩個必備條件:(引用)。

      • 一個物件在其生存的上下文環境之外,即會失效。

      • 一個全域性的物件在沒有被執用(引用)的情況下,即會失效

事件優化


  • 使用事件代理

    • 當存在多個元素需要註冊事件時,在每個元素上繫結事件本身就會對效能有一定損耗。

    • 由於DOM Level2事件模 型中所有事件預設會傳播到上層文件物件,可以藉助這個機制在上層元素註冊一個統一事件對不同子元素進行相應處理。

捕獲型事件先發生。兩種事件流會觸發DOM中的所有物件,從document物件開始,也在document物件結束。

    <ul id="parent-list">
        <li id="post-1">Item 1
        <li id="post-2">Item 2
        <li id="post-3">Item 3
        <li id="post-4">Item 4
        <li id="post-5">Item 5
        <li id="post-6">Item 6
    </li></ul>
    // Get the element, add a click listener...
    document.getElementById("parent-list").addEventListener("click",function(e) {
        // e.target is the clicked element!
        // If it was a list item
        if(e.target && e.target.nodeName == "LI") {
            // List item found!  Output the ID!
            console.log("List item ",e.target.id.replace("post-")," was clicked!");
        }
    });

陣列專題


  • 當需要使用陣列時,可使用JSON格式的語法

    • 即直接使用如下語法定義陣列:[parrm,param,param...],而不是採用new Array(parrm,param,param...)這種語法。使用JSON格式的語法是引擎直接解釋。而後者則需要呼叫Array的構造器。

  • 如果需要遍歷陣列,應該先快取陣列長度,將陣列長度放入區域性變數中,避免多次查詢陣列長度。

    • 根據字串、陣列的長度進行迴圈,而通常這個長度是不變的,比如每次查詢a.length,就要額外進行一個操作,而預先把var len=a.length,則每次迴圈就少了一次查詢。

同域跨域


  • 避免跳轉

    • 同域:注意避免反斜槓 “/” 的跳轉;

    • 跨域:使用Alias或者mod_rewirte建立CNAME(儲存域名與域名之間關係的DNS記錄)

效能測試工具


  • js效能優化和記憶體洩露問題及檢測分析工具

    • 效能優化ajax工具diviefirebug

    • [web效能分析工具YSlow]

      • performance效能評估打分,右擊箭頭可看到改進建議。

      • stats快取狀態分析,傳輸內容分析。

      • components所有載入內容分析,可以檢視傳輸速度,找出頁面訪問慢的瓶頸。

      • tools可以檢視js和css,並列印頁面評估報告。

    • 記憶體洩露檢測工具sIEve

      • sIEve是基於IE的記憶體洩露檢測工具,需要下載執行,可以檢視dom孤立節點和記憶體洩露及記憶體使用情況。

        1. 列出當前頁面內所有dom節點的基本資訊(html id style 等)

        2. 頁面內所有dom節點的高階資訊 (記憶體佔用,數量,節點的引用)

        3. 可以查找出頁面中的孤立節點

        4. 可以查找出頁面中的迴圈引用

        5. 可以查找出頁面中產生記憶體洩露的節點

    • 記憶體洩露提示工具leak monitor

      • leak monitor在安裝後,當離開一個頁面時,比如關閉視窗,如果頁面有記憶體洩露,會彈出一個文字框進行即時提示。

    • 程式碼壓縮工具

      • YUI壓縮工具

      • Dean Edwards Packer

      • JSMin

    • Blink/Webkit瀏覽器

      • Blink/Webkit瀏覽器中(ChromeSafariOpera),我們可以藉助其中的Developer ToolsProfiles工具來對我們的程式進行記憶體檢查。

        Developer Tools - Profiles
  • Node.js中的記憶體檢查

    • Node.js中,我們可以使用node-heapdumpnode-memwatch模組進​​行記憶體檢查。

      var heapdump = require('heapdump');
      var fs = require('fs');
      var path = require('path');
      fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);

    在業務程式碼中引入node-heapdump之後,我們需要在某個執行時期,向Node.js程序傳送SIGUSR2訊號,讓node-heapdump抓拍一份堆記憶體的快照。

       $ kill -USR2 (cat app.pid)
    
    這樣在檔案目錄下會有一個以`heapdump-<sec>.<usec>.heapsnapshot`格式命名的快照檔案,我們可以使用瀏覽器的`Developer Tools`中的`Profiles`工具將其開啟,並進行檢查。
  • 分析瀏覽器提供的Waterfall圖片來思考優化入口。

  • 新的測試手段(Navigation, Resource, 和User timing。

迴圈專題


  • 迴圈是一種常用的流程控制。

    • JAVASCRIPT提供了三種迴圈。

      • for(;;)

        • 推薦使用for迴圈,如果迴圈變數遞增或遞減,不要單獨對迴圈變數賦值,而應該使用巢狀的++–-運算子。

        • 程式碼的可讀性對於for迴圈的優化。

        • -=1

        • 從大到小的方式迴圈(這樣缺點是降低程式碼的可讀性)。

          /效率低
          var divs = document.getElementsByTagName("div"); 
          for(var i = 0; i < divs.length; i++){

          ...


          /效率高,適用於獲取DOM集合,如果純陣列則兩種情況區別不到
          var divs = document.getElementsByTagName("div"); 
          for(var i = 0, len = divs.length; i < len; i++){

          ...

          }
          /IE6.0下,for(;;)迴圈在執行中,第一種情況會每次都計算一下長度,而第二種情況卻是在開始的時候計算長度,並把其儲存到一個變數中,所以其執行效率要高點,所以在我們使用for(;;)迴圈的時候,特別是需要計算長度的情況,我們應該開始將其儲存到一個變數中。/

      • while()

        • for(;;)while()迴圈的效能基本持平。

      • for(in)

        • 在這三種迴圈中for(in)內部實現是構造一個所有元素的列表,包括array繼承的屬性,然後再開始迴圈,並且需要查詢hasOwnProperty。所以for(in)相對for(;;)迴圈效能要慢。

  • 選擇正確的方法

    • 避免不必要的屬性查詢。

      • 訪問變數陣列O(1)操作。

      • 訪問物件上的屬性是一個O(n)操作。

        物件上的任何屬性查詢都要比訪問變數或陣列花費更長時間,因為必須在原型鏈中對擁有該名稱的屬性進行一次搜尋,即屬性查詢越多,執行時間越長。所以針對需要多次用到物件屬性,應將其儲存在區域性變數。
        
    • 優化迴圈。

      • 減值迭代。

        • 大多數迴圈使用一個從0開始,增加到某個特定值的迭代器。在很多情況下,從最大值開始,在迴圈中不斷減值的迭代器更加有效。

      • 簡化終止條件。

        • 由於每次迴圈過程都會計算終止條件,故必須保證它儘可能快,即避免屬性查詢或其它O(n)的操作。