高性能JavaScript

前言

本文基於《高性能JavaScript》整理而成。javascript

加載和運行

背景

  • 不管是<script>標籤引用的外部js文件,仍是內聯的<script>標籤,都會阻塞其餘瀏覽器的處理過程,直到js代碼被「下載--解析--執行」完成後,纔會繼續其餘進程。
  • 部分高級瀏覽器已經支持並行下載js文件,但瀏覽器進程仍然須要等待全部js文件執行完畢後,纔會繼續。
  • 動態建立的<script>標籤不會阻塞頁面的解析。
  • 頁面解析時,在遇到<body>前,頁面是空白的。

優化方法

  • 阻塞方式
    • 將全部<script>標籤放置在頁面的底部,僅靠</body>的上方。此方法能夠保證頁面在腳本運行前完成解析。
    • 將腳本成組打包。css

      頁面的<script>標籤越少,頁面的加載速度越快,響應也更加迅速。不論外部腳本文件仍是內聯代碼都是如此。
  • 非阻塞方式
    • <script>標籤添加defer屬性(只適用於IEFirefox 3.5以上的版本)html

      這種方式引入的js代碼會在domReady後執行
    • 動態建立<scirpt>元素,用它下載並執行代碼java

      動態建立的<script>不會阻塞頁面的解析,js代碼的處理和頁面的解析是並行的
    • ajax下載代碼,注入頁面中ajax

      ajax方式的缺點是不能跨域獲取js代碼算法

數據

詳情

  • 做用域鏈
    • 背景
      • 函數對象編程

        建立函數時,會建立一個函數對象,並建立一個做用域鏈(內部[[scope]]屬性json

      • 每執行一次函數,就建立一個運行上下文windows

        運行上下文也會建立一個做用域鏈,並將函數對象的做用域鏈賦值到運行上下文,再新建一個活動對象,置於做用域鏈的第一個位置。跨域

        做用域鏈:
        • 0:新建的活動對象
        • 1:函數對象的做用域鏈複製過來

        做用域鏈銷燬時,活動對象額一同銷燬

      • 做用域鏈的查找性能
        • 局部變量的訪問速度老是最快的,由於它們位於做用域鏈的第一個位置
        • 而全局變量一般是最慢的(優化的JS引擎在某些狀況下能夠改變這種情況),由於它們位於做用域鏈的末端。
    • 優化
      • 在沒有優化JS引擎的瀏覽器中,最好儘量使用局部變量。用局部變量存儲本地範圍以外的變量值,若是它們在函數中的使用多餘一次
  • 改變做用域鏈
    • 背景
      • with
        • 代碼流執行到一個with表達式時,運行期上下文的做用域鏈被臨時改變。一個新的可變對象被建立,它包含指定對象的全部屬性,此對象被推入做用域鏈的簽到,意味着如今函數的全部局部變量被推入第二個做用域鏈對象中,全部訪問代價更高
      • try catch
        • catch塊中,會將異常對象推入做用域鏈簽到的一個可變對象中
        • 只要catch執行完畢,做用域鏈會返回到原來的狀態
    • 優化
      • 不使用with
      • 謹慎使用try catch

        能夠精簡代碼最小化catch對性能的影響,一個很好的模式是將錯誤交給一個專用函數來處理。沒有局部變量訪問,做用域鏈臨時改變不會影響代碼的性能。

    • 動態做用域
      • 背景

        優化的JS引擎是經過分析靜態代碼來肯定哪些變量應該在任意時刻被訪問,企圖避開傳統的做用域鏈查找,取代以標識符索引的方式進行快速查找。當涉及一個動態做用域後,此優化方法就不起做用了。引發須要切回慢速的寄語哈希表的標識符識別方法,更像傳統的做用域鏈搜索
      • 優化
        • 避免使用動態做用域
  • 閉包
    • 這裏的閉包指的是活動對象裏建立的函數對象
    • 外層的執行上下文的做用域鏈包括:活動對象、全局對象;
    • 閉包的做用域鏈包括:活動對象、全局對象
    • 外層函數執行完畢後,執行上下文銷燬,但活動對象仍然被閉包的做用域鏈引用,所以不會銷燬,這樣就有性能開銷。尤爲在IE中更被關注,IE使用非本地JS對象實現DOM對象,閉包可能致使內存泄露
  • 對象成員
    • 背景
      • 對象成員比直接量或局部變量訪問速度慢,在某些瀏覽器上比訪問數組項還慢
        • 對象有兩種類型的成員:實例成員和原型成員
        • hasOwnProperty()訪問的是實例成員
        • in訪問的是實例+原型成員
        • 增長遍歷原型鏈的開銷很大
    • 優化
      • 只在必要狀況下使用對象成員
      • 用局部遍歷存儲對象成員,局部變量要快不少

總結

  • 數據存儲位置能夠對代碼總體性能產生重要影響
  • 四種數據訪問類型:
    • 直接量
    • 變量
    • 數組項
    • 對象成員
  • 直接量和局部變量的訪問速度很是快,數組項和對象成須要更長時間
  • 避免使用with表達式,由於它該變量運行期上下文的做用域鏈。
  • 當心對的try-catch表達式的catch語句,由於它有一樣的效果
  • 嵌套對象成員會形成重大性能影響,儘可能少用
  • 一個屬性或方法在原型鏈中的位置越深,訪問它的速度就越慢
  • 通常來講,能夠經過如下方法提升性能:

    將常常用到的對象成員,數組項和域外變量存入局部變量中,而後,訪問局部變量的速度會快於那些原始變量

DOM編程

詳情

  • 什麼是DOM?
    • DOM 是與語言無關的API,瀏覽器中的接口倒是以JavaScript實現的
    • 瀏覽器一般要求DOM實現和JavaScript實現保持相互獨立
      • IE
        • JavaScript實現:位於庫jscript.dll
        • DOM實現:位於另外一個庫mshtml.dll(內部代號Trident)
      • Safari
        • JavaScript實現:JavaScriptCore引擎
        • DOM實現:WebkitWebCore處理
      • Chrome
        • JavaScript實現:V8引擎
        • DOM實現:WebkitWebCore處理
      • Firefox
        • JavaScript實現:TraceMonkey引擎
        • DOM實現:Gecko渲染引擎
    • DOM天生就慢
      • 兩個獨立的部分以功能接口鏈接就會帶來性能損耗
  • DOM訪問和修改
    • 訪問速度就很慢了,修改更慢
    • 訪問的DOM越多,代碼的執行速度就越慢
  • innerHTMLDOM方法對比
    • innerHTML不是標準的,但被支持的很好
    • DOM方法有:document.createElement()
    • 兩者的性能差異並不大,但在全部瀏覽器中,innerHTML速度更快,除了最新的基於WebKit的瀏覽器
    • 從性能上沒有必要區分兩者,更多的是從編碼風格、可讀性、團隊習慣等等方面考慮
  • 節點克隆(element.cloneNode()
    • 大多數瀏覽器中,克隆節點更有效率,但提升很少
  • HTML集合
    • 指的是如document.getElementsByTagName()得到的元素集
    • 具備length屬性,但不是數組
    • 屢次訪問元素集的過程當中,元素集增刪節點,也會即時反映在其length屬性上
    • 優化方法
      • 用局部變量緩存length
      • 用局部變量緩存集合中的元素
  • 選取更有效的API
    • 抓取DOM
      • childNodes
      • nextSibling
      • IE中,nextSibling的效率更高,其餘狀況下,沒太多差異
      • childNodesfirstChildnextSibling也會返回註釋節點和文本節點,所以每次使用都要判斷節點類型,比較麻煩
      • 如下API只返回元素節點(如下API中,IE678只支持children
        • children替代childNodeschildren更快,由於集合項更少
        • childElementCount替代childNodes.length
        • firstElementChild替代firstChild
        • lastElementChild替代lastChild
        • nextElementSibling替代nextSibling
        • previousElementSibling替代previousSibling
      • CSS選擇器
        • 最新的瀏覽器有(IE8及以上)
        • document.querySelectorAll()
          • 返回一個類數組對象,不返回HTML集合,因此返回的節點不呈現文檔的「存在性結構」,也就避免了前面的HTML集合所固有的性能問題
  • 重繪和排版
    • 背景
      • DOM樹和渲染數
        • 當瀏覽器下載完全部的頁面HTML標記,javascript、css、圖片以後,它解析文件並建立兩個內部數據結構:DOM樹和渲染樹
        • DOM樹表示頁面結構,渲染樹表示DOM節點如何顯示
        • 渲染樹中爲每一個須要顯示的DOM樹節點至少存放一個節點(隱藏DOM元素在渲染樹中沒有節點)
      • 重繪和排版是不一樣的概念
      • 不是全部的DOM改變都會影響幾何屬性
      • 重繪和排版是負擔很重的操做,可能致使網頁應用的用戶界面失去響應
      • 會引起重排版的操做
        • 小範圍影響
          • 添加或刪除可見的DOM元素
          • 元素位置改變
          • 元素尺寸改變
          • 內容改變(文本改變或圖片被另外一個不一樣尺寸的所替代)
          • 最初的頁面渲染
          • 瀏覽器窗口改變尺寸
        • 影響整個頁面的
          • 滾動條出現
      • 查詢佈局信息
        • 任何查詢都會刷新渲染隊列,大部分瀏覽器都會批量處理這些隊列
    • 優化
      • 批量修改風格
        • 統一處理
        • 修改CSS的類名
      • 離線操做DOM樹
        • 有三個方法能夠將DOM從文檔中摘除
          • 隱藏元素,而後修改,而後顯示
          • 使用文檔片段
          • 將原始元素拷貝到一個脫離文檔的節點中,修改副本,而後覆蓋原始元素
      • 緩存並減小對佈局信息的訪問
      • 將元素提出動畫流
        • 絕對定位
  • IE和:hover
    • 不要對大量元素應用:hover
  • 採用事件託管

總結

  • 最小化DOM訪問,在JavaScript端作儘量多的事情
  • 在反覆訪問的地方使用局部變量存放DOM引用
  • 當心處理HTML集合
    • 集合老是會對底層文檔從新查詢
    • 緩存length屬性
    • 若是常常操做集合,能夠將集合拷貝到數組中
  • 採用更快的API
  • 注意重繪和排版
    • 批量修改風格
    • 離線操做DOM樹
    • 緩存並減小對佈局信息的訪問
  • 動畫中使用絕對座標
  • 使用事件代理最小化句柄數量

算法和流程控制

詳情

  • 前言
    • 代碼總體結構是執行速度的決定因素之一
    • 代碼量少不必定運行速度快,代碼量大不必定運行速度慢
  • 四種循環
    • for
      • 包括四部分:初始化體、前測條件、後執行體、循環體
    • while
      • 包括兩部分:預測試條件、循環體
    • do while
      • js中惟一一種後測試的循環,包括:循環體和後測試條件
    • for in
      • 用途:枚舉任何對象的實例屬性和原型屬性
  • 循環性能
    • for in速度最慢,由於它要查找各類屬性
      • 優化

        若是要迭代一個有限的、已知的屬性列表,使用其餘循環類型更快,可以使用以下模式(只關注感興趣的屬性):

        var props = ["prop1", "prop2"],
            i = 0;
        while (i < props.length){
            process(object[props[i]]);
        }
    • 其餘循環性能至關
      • 減小迭代的工做量
      • 減小迭代次數
        • 達夫設備
    • 基於函數的迭代
      • foreach每次迭代都會調用函數,性能較低
  • 條件表達式
    • 兩種條件表達式
      • if else
      • switch
    • 如何選擇
      • 基於條件數量
        • 易讀性:條件數量較大,傾向於使用switch
        • 性能:switch更快
      • 優化if else
        • 將最多見的條件體放在首位
        • if else組織成一系列嵌套的if else表達式。使用一個單獨的一長串的if else一般致使運行緩慢,由於每一個條件都要被計算
          • 好比使用二分法
        • 查表法
          • 暫不瞭解
  • 遞歸
    • 遞歸的問題
      • 一個錯誤定義,或者缺乏終結條件可致使長時間運行,凍結用戶界面
      • 還會遇到瀏覽器調用棧大小的限制
    • 優化
      • 任何能夠用遞歸實現的算法均可以用迭代實現。使用優化的循環替代長時間運行的遞歸函數能夠提升性能,由於運行一個循環比反覆調用一個函數的開銷要低
      • 製表
        • 記錄計算過的結果

總結

  • 代碼的寫法和算法選用會影響JavaScript的運行時間。與其餘語言不一樣的是,JavaScript可用資源有限,因此優化技術更爲重要
  • forwhiledo-while 循環的性能特性類似
  • 除非要迭代一個屬性未知的對象,不然不要使用for-in循環
  • 改善循環性能的最好辦法是減小每次迭代中的運算量,並減小循環迭代次數
  • 通常來講,switch老是比if-else更快,但並不老是最好的解決辦法
  • 當判斷條件較多時,查表法比if-else或者switch更快
  • 瀏覽器的調用棧尺寸限制了遞歸算法在JavaScript中的應用:棧溢出錯誤致使其餘代碼也不能正常執行
  • 若是使用遞歸,修改成一個迭代算法或者使用製表法能夠避免重複工做
  • 運行的代碼總量越大,使用這些策略所帶來的性能提高就越明顯

響應接口

詳情

  • 瀏覽器有一個單獨的處理進程,它由兩個任務所共享:
    • JavaScript 任務
    • 用戶界面更新任務
    • 每一個時刻只有其中的一個操做得以執行,也就是JavaScript代碼運行時用戶界面不能對輸入產生反應,反之亦然。管理好JS運行時間對網頁應用的性能很重要
  • 瀏覽器 UI 線程
    • JSUI更新共享的進程一般被稱做瀏覽器UI線程。
    • 此UI線程圍繞一個簡單的隊列系統工做,任務被保存到隊列中直至進程空閒。一旦空閒,隊列中的下一個任務將被檢索和運行。這些任務不是運行JS代碼,就是執行UI更新,包括重繪和排版
  • 瀏覽器有兩個限制
    • 調用棧尺寸限制
    • 長時間腳本限制
      • 每一個瀏覽器對長運行腳本檢查方法上略有不一樣
      • 多久算「過久」?
        • 一個單一的JS操做應當使用的總時間(最大)是100毫秒
  • 用定時器讓出時間片
    • 若是有些JS任務由於複雜性緣由不能在100毫秒或更少的時間內完成,這種狀況下,理想方法是讓出對UI線程的控制,讓UI更新能夠進行,讓出控制意味着中止JS運行,給UI線程機會進行更新,而後再運行`JS
    • 定時器setTimeout到達時間後,只是加入隊列,並非執行
  • 定時器精度
    • 瀏覽器的定時器不是精確的,一般會發生幾毫秒偏移
    • windows 系統上定時器分辨率爲15毫秒
      • 定時器小於15將在IE中致使瀏覽器鎖定,因此最小值建議爲25毫秒(實際時間是15或30)以確保至少15毫秒延遲
      • 大多數瀏覽器在定時器延時小於10毫秒時表現出差別性
  • 在數組處理中使用定時器
    • 循環優化技巧若是還不能達到目標,能夠考慮使用定時器,考慮如下條件:
      • 處理過程必須是同步處理嗎?
      • 數據必須按順利處理嗎?
    • 若是上述答案都是否,則可使用定時器優化
  • 分解任務
    • 若是一個函數運行時間太長,能夠考慮分解趁改一系列可以短期完成的較小的函數,把獨立方法放在定時器中調用。將每一個函數放入一個數組,而後用上面講到的數組處理模式。
  • 限時運行代碼
    • 根據以上描述,每次定時器只執行一個任務效率不高。
    • 優化方法是:每次定時器執行多個任務,設定時間限制小於50毫秒便可(do-while循環)
  • 定時器性能
    • 低頻率的重複定時器(間隔在1秒或1秒以上),幾乎不影響整個網頁應用的響應
    • 多個重複定時器使用更高的頻率(間隔在100到200毫秒之間)性能更低
    • 優化
      • 限制高頻率重複定時器的數量
      • 建立一個單獨的重複定時器,每次執行多個操做
  • 網絡工人線程
    • 暫無

總結

JavaScript和用戶界面更新在同一個進程內運行,同一時刻只有其中一個能夠運行。有效地管理UI線程就是要確保JavaScript不能運行太長時間,一面影響用戶體驗。所以要注意:

  • JavaScript運行時間不該該超過100毫秒,過長的運行時間致使UI更新出現可察覺的延遲,從而對總體用戶體驗產生負面影響
  • JavaScript運行期間,瀏覽器響應用戶交互的行爲存在差別,不管如何,JavaScript長時間運行將致使用戶體驗混亂和脫節
  • 定時器能夠用於安排代碼推遲執行,它使得你能夠將長運行腳本分解成一系列較小的任務
  • 網絡工人線程是新式瀏覽器才支持的特性,它容許你在UI線程以外運行JavaScript代碼而避免鎖定UI
  • 網絡應用程序越複雜,積極主動地管理UI線程就越顯得重要。沒有什麼JavaScript代碼能夠重要到容許影響用戶體驗的程度

異步JavaScript

詳情

  • 有五種經常使用技術用於向服務器請求數據
    • XMLHttpRequest(XHR)(經常使用)
    • 動態腳本標籤插入(經常使用)
    • Multipart XHR(經常使用)
    • iframes(不經常使用)
    • Comet(不經常使用)
  • XHR
    • 就是ajax
    • 不能跨域
    • 能夠選擇GETPOST
    • GET
      • 若是不改變服務器狀態只是取回數據,則使用GET
      • GET請求會被緩存
    • POST
      • 當URL和參數的長度超過了2048個字符時才使用POST提取數據
  • 動態腳本插入(jsonp
    • 能夠跨域
    • 只能經過GET方法傳遞,不能用POST
    • 對服務器返回的數據格式有要求
  • Multipart XHR
    • 暫略
  • 若是隻向服務器發送數據,有兩種技術
    • XHR
      • XHR主要用於從服務器獲取數據,它也能夠用來向服務器發送數據
      • 能夠用GETPOST方式發送數據,以及任意數量的HTTP信息頭。這樣靈活性大。當數據量超過瀏覽器的最大URL長度時,XHR特別有用。這時候能夠用POST方式發送數據
      • 向服務器發送數據時,GETPOST快。
        • GET請求要佔用一個單獨的數據包
        • POST至少要發送兩個數據包,一個用於信息頭,一個是POST體
    • 燈標
      • 和動態腳本標籤插入相似,用新的Image對象,將src設置爲服務器上一個腳本文件的URL
      • Image對象沒必要插入DOM節點
      • 這是將信息發回服務器的最有效方法。開銷最小,並且任何服務器端錯誤都不會影響客戶端
      • 限制
        • 不能發送POST數據
        • 除了onload,不多能獲取服務器返回的信息
  • 數據格式
    • 越輕量級的格式越好,最好是JSON和字符分隔的自定義格式。數據量大的話,就用這兩種格式
  • 其餘優化技術
    • 避免發出沒必要要的Ajax請求
      • 在服務端,設置HTTP頭,確保返回報文被緩存在瀏覽器中
      • 在客戶端,於本地緩存已獲取的數據,不要屢次請求同一個數據
    • 服務端
      • 若是想要緩存Ajax響應報文,客戶端發起請求必須使用GET方法
      • 設置Expires

總結

  • 高性能Ajax包括:知道你項目的具體需求,選擇正確的數據格式和與之相配的傳輸技術
  • 數據格式
    • 純文本和HTML是高度限制的,但它們可節省客戶端的CPU週期
    • XML被普遍支持,但它很是冗長且解析緩慢
    • JSON是輕量級的,解析迅速(做爲本地代碼而不是字符串),交互性與XML至關
    • 字符分隔的自定義格式很是輕量,在大量數據解析時速度最快,但要額外地編寫程序在服務端構造格式,並在客戶端解析
  • 請求數據
    • XHR提供最完善的控制和靈活性,儘管它將全部傳入數據視爲一個字符串,這有可能下降解析速度
    • jsonp容許跨域,但接口不夠安全,並且不能讀取信息頭或響應報文代碼
    • MXHR能夠減小請求的數量,一次響應中處理不一樣的文件類型,儘管它不能緩存收到的響應報文
  • 發送數據
    • 圖像燈標是最簡單和最有效的方法
    • XHR也能夠用POST方法發送大量數據
  • 其餘準則提升Ajax的速度
    • 減小請求數量,可經過JavaScriptCSS打包,或者使用MXHR
    • 縮短頁面的加載時間,在頁面其餘內容加載以後,使用Ajax獲取少許重要文件
    • 確保代碼錯誤不要直接顯示給用戶,並在服務器端處理錯誤
    • 學會什麼時候使用一個健壯的Ajax庫,什麼時候編寫本身的底層Ajax代碼
  • Ajax是提高網站性能的最大的改進區域之一

編程實踐

詳情

  • 避免二次評估
    • JavaScript容許在程序中獲取一個包含代碼的字符串而後運行它
    • 有四種標準方法能夠實現
      • eval_r()
      • Function()構造器
      • setTimeout()
      • setInterval()
    • 這樣的話,會有兩步:字符串首先被評估爲正常代碼,而後執行過程當中,運行字符串中的代碼時發生另外一次評估。二次評估是昂貴的操做
  • 使用對象/數組直接量
  • 不要重複工做
    • 不要作沒必要要的工做
    • 不要重複已經完成的工做
  • 延遲加載
  • 使用速度快的部分
    • 引擎一般是處理過程當中最快的部分,實際上速度慢的是你的代碼
  • 位操做運算符
    • 暫略
  • 使用原生方法
    • 內置的Math屬性
      • Math.E
      • Math.LN10
      • Math.LN2
      • Math.LOG2E
      • Math.LOG10E
      • Math.PI
      • Math.SQRT1_2
      • Math.SQRT2
    • 內置的Math方法
      • Math.abs(num)
      • Math.exp(num)
      • Math.log(num)
      • Math.pow(num, power)
      • Math.sqrt(num)
      • Math.acos(x)
      • Math.asin(x)
      • Math.atan(x)
      • Math.atan2(y, x)
      • Math.cos(x)
      • Math.sin(x)
      • Math.tan(x)
    • 原生的CSS選擇器API
      • querySelector()
      • querySelectorAll()

總結

  • 避免使用eval_r()Function()構造器避免二次評估,此外,給setTimeout()setInterval()傳遞函數參數而不是字符串參數
  • 建立新對象和數組時使用對象直接量和數組直接量。它們比非直接量形式建立和初始化更快
  • 避免重複進行相同工做。當須要檢測瀏覽器時,使用延遲加載或條件預加載
  • 執行數學運算時,考慮使用位操做,它直接在數字底層進行操做
  • 原生方法老是比JavaScript寫的東西要快。儘可能使用原生方法。

建立並部署高性能JavaScript應用程序

  • 合併JavaScript文件,減小HTTP請求的數量
  • 壓縮JS文件
  • 經過設置HTTP
  • 相應報文頭使JS文件可緩存,經過向文件名附加時間戳解決緩存問題
  • 使用CDN提供JS文件,CDN不只能夠提升性能,還能夠爲你管理壓縮和緩存
相關文章
相關標籤/搜索