前端性能優化:細說JavaScript的加載與執行

本文主要是從性能優化的角度來探討JavaScript在加載與執行過程當中的優化思路與實踐方法,既是細說,文中在涉及原理性的地方,難免會多說幾句,還望各位讀者保持耐心,仔細理解,請相信,您的耐心付出必定會讓您獲得與之匹配的回報。javascript

緣起

隨着用戶體驗的日益重視,前端性能對用戶體驗的影響備受關注,但因爲引發性能問題的緣由相對複雜,咱們很難但從某一方面或某幾個方面來全面解決它,這也是我行此文的緣由,想以此文爲起點,用一系列文章來深層次探討與梳理有關Javascript性能的方方面面,以填補並夯實本身的知識結構。css

目錄結構

本文大體的行文思路,包含但不侷限:html

  • 不得不說的JavaScript阻塞特性前端

  • 合理放置腳本位置,以優化加載體驗,js腳本放在 <body>標籤閉合以前。java

  • 減小HTTP請求次數,壓縮精簡腳本代碼。node

  • 無阻塞加載JavaScript腳本:web

    • 使用<script>標籤的defer屬性。後端

    • 使用HTML5的async屬性。跨域

    • 動態建立<script>元素加載JavaScript。瀏覽器

    • 使用XHR對象加載JavaScript。

不得不說的JavaScript的阻塞特性

前端開發者應該都知道,JavaScript是單線程運行的,也就是說,在JavaScript運行一段代碼塊的時候,頁面中其餘的事情(UI更新或者別的腳本加載執行等)在同一時間段內是被掛起的狀態,不能被同時處理的,因此在執行一段js腳本的時候,這段代碼會影響其餘的操做。這是JavaScript自己的特性,咱們沒法改變。

咱們把JavaScript的這一特性叫作阻塞特性,正由於這個阻塞特性,讓前端的性能優化尤爲是在對JavaScript的性能優化上變得相對複雜。

爲何要阻塞?

也許你還會問,既然JavaScript的阻塞特性會產生這麼多的問題,爲何JavaScript語言不能像Java等語言同樣,採用多線程,不就OK了麼?

要完全理解JavaScript的單線程設計,其實並不難,簡單總結就是:最初設計JavaScript的目的只是用來在瀏覽器端改善網頁的用戶體驗,去處理一些頁面中相似表單驗證的簡單任務。因此,那個時候JavaScript所作的事情不多,而且代碼不會太多,這也奠基了JavaScript和界面操做的強關聯性。

既然JavaScript和界面操做強相關,咱們不妨這樣理解:試想,若是在某個頁面中有兩段js腳本都會去更改某一個dom元素的內容,若是JavaScript採用了多線程的處理方式,那麼最終頁面元素顯示的內容究竟是哪一段js腳本操做的結果就不肯定了,由於兩段js是經過不一樣線程加載的,咱們沒法預估誰先處理完,這是咱們不想要的結果,而這種界面數據更新的操做在JavaScript中比比皆是。所以,咱們就不難理解JavaScript單線程的設計緣由:JavaScript採用單線程,是爲了不在執行過程當中頁面內容被不可預知的重複修改

關於JavaScript的更多「身世」之謎,能夠看阮一峯老師的Javascript誕生記

從加載上優化:合理放置腳本位置

因爲JavaScript的阻塞特性,在每個<script>出現的時候,不管是內嵌仍是外鏈的方式,它都會讓頁面等待腳本的加載解析和執行,而且<script>標籤能夠放在頁面的<head>或者<body>中,所以,若是咱們頁面中的css和js的引用順序或者位置不同,即便是一樣的代碼,加載體驗都是不同的。舉個栗子:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>js引用的位置性能優化</title>
    <script type="text/javascript" src="index-1.js"></script>
    <script type="text/javascript" src="index-2.js"></script>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
複製代碼

以上代碼是一個簡單的html界面,其中加載了兩個js腳本文件和一個css樣式文件,因爲js的阻塞問題,當加載到index-1.js的時候,其後面的內容將會被掛起等待,直到index-1.js加載、執行完畢,纔會執行第二個腳本文件index-2.js,這個時候頁面又將被掛起等待腳本的加載和執行完成,一次類推,這樣用戶打開該界面的時候,界面內容會明顯被延遲,咱們就會看到一個空白的頁面閃過,這種體驗是明顯很差的,所以咱們應該儘可能的讓內容和樣式先展現出來,將js文件放在<body>最後,以此來優化用戶體驗

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>js引用的位置性能優化</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="index-1.js"></script>
    <script type="text/javascript" src="index-2.js"></script>
  </body>
</html>
複製代碼

從請求次數上優化: 減小請求次數

有一點咱們須要知道:頁面加載的過程當中,最耗時間的不是js自己的加載和執行,相比之下,每一次去後端獲取資源,客戶端與後臺創建連接纔是最耗時的,也就是大名鼎鼎的Http三次握手,固然,http請求不是咱們這一次討論的主題,想深刻了解的自行搜索,網絡上相關文章不少。

所以,減小HTTP請求,是咱們着重優化的一項,事實上,在頁面中js腳本文件加載很不少狀況下,它的優化效果是很顯著的。要減小HTTP的請求,就不得不提起文件的精簡壓縮了。

文件的精簡與壓縮

要減小訪問請求,則必然會用到js的**精簡(minifucation)和壓縮(compression)**了,須要注意的是,精簡文件實際並不複雜,但不適當的使用也會致使錯誤或者代碼無效的問題,所以在實際的使用中,最好在壓縮以前對js進行語法解析,幫咱們避免沒必要要的問題(例如文件中包含中文等unicode轉碼問題)。

解析型的壓縮工具經常使用有三:YUI Compressor、Closure Complier、UglifyJs

YUI Compressor: YUI Compressor的出現曾被認爲是最受歡迎的基於解析器的壓縮工具,它將去去除代碼中的註釋和額外的空格而且會用單個或者兩個字符去代替局部變量以節省更多的字節。但默認會關閉對可能致使錯誤的替換,例如with或者eval();

Closure Complier: Closure Complier一樣是一個基於解析器的壓縮工具,他會試圖去讓你的代碼變得儘量小。它會去除註釋和額外的空格並進行變量替換,並且會分析你的代碼進行相應的優化,好比他會刪除你定義了但未使用的變量,也會把只使用了一次的變量變成內聯函數。

UglifyJs:UglifyJs被認爲第一個基於node.js的壓縮工具,它會去除註釋和額外的空格,替換變量名,合併var表達式,也會進行一些其餘方式的優化

每種工具都有本身的優點,好比說YUI壓縮後的代碼準確無誤,Closure壓縮的代碼會更小,而UglifyJs不依靠於Java而是基於JavaScript,相比Closure錯誤更少,具體用哪一個更好我以爲沒有個確切的答案,開發者應該根據本身項目實際狀況酌情選擇。

從加載方式上優化:無阻塞腳本加載

在JavaScript性能優化上,減小腳本文件大小並限制HTTP請求的次數僅僅是讓界面響應迅速的第一步,如今的web應用功能豐富,js腳本愈來愈多,光靠精簡源碼大小和減小次數不老是可行的,即便是一次HTTP請求,但文件過於龐大,界面也會被鎖死很長一段時間,這明顯很差的,所以,無阻塞加載技術應運而生。

簡單來講,就是頁面在加載完成後才加載js代碼,也就是在window對象的load事件觸發後纔去下載腳本。 要實現這種方式,經常使用如下幾種方式:

延遲腳本加載(defer)

HTML4之後爲<script>標籤訂義了一個擴展屬性:defer。defer屬性的做用是指明要加載的這段腳本不會修改DOM,所以代碼是能夠安全的去延遲執行的,而且如今主流瀏覽器已經所有對defer支持。

<script type="text/javascript" src="index-1.js" defer></script>
複製代碼

帶defer屬性的<script>標籤在DOM完成加載以前都不會去執行,不管是內嵌仍是外鏈方式。

延遲腳本加載(async)

HTML5規範中也引入了async屬性,用於異步加載腳本,其大體做用和defer是同樣的,都是採用的並行下載,下載過程當中不會有阻塞,但不一樣點在於他們的執行時機,async須要加載完成後就會自動執行代碼,可是defer須要等待頁面加載完成後纔會執行

從加載方式上優化:動態添加腳本元素

把代碼以動態的方式添加的好處是:不管這段腳本是在什麼時候啓動下載,它的下載和執行過程都不會則色頁面的其餘進程,咱們甚至能夠直接添加帶頭部head標籤中,都不會影響其餘部分。

所以,做爲開發的你確定見到過諸如此類的代碼塊:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
複製代碼

這種方式即是動態建立腳本的方式,也就是咱們如今所說的動態腳本建立。經過這種方式下載文件後,代碼就會自動執行。可是在現代瀏覽器中,這段腳本會等待全部動態節點加載完成後再執行。這種狀況下,爲了確保當前代碼中包含的別的代碼的接口或者方法可以被成功調用,就必須在別的代碼加載前完成這段代碼的準備。解決的具體操做思路是:

現代瀏覽器會在script標籤內容下載完成後接收一個load事件,咱們就能夠在load事件後再去執行咱們想要執行的代碼加載和運行,在IE中,它會接收loaded和complete事件,理論上是loaded完成後纔會有completed,但實踐告訴咱們他兩彷佛並無個前後,甚至有時候只會拿到其中的一個事件,咱們能夠單獨的封裝一個專門的函數來體現這個功能的實踐性,所以一個統一的寫法是:

function LoadScript(url, callback) {
        var script = document.createElement('script');
        script.type = 'text/javascript';

        // IE瀏覽器下
        if (script.readyState) {
          script.onreadystatechange = function () {
            if (script.readyState == 'loaded' || script.readyState == 'complete') {
              // 確保執行兩次
              script.onreadystatechange = null;
              // todo 執行要執行的代碼
              callback()
            }
          }
        } else {
          script.onload = function () {
            callback();
          }
        }

        script.src = 'file.js';
        document.getElementsByTagName('head')[0].appendChild(script);
      }

複製代碼

LoadScript函數接收兩個參數,分別是要加載的腳本路徑和加載成功後須要執行的回調函數,LoadScript函數自己具備特徵檢測功能,根據檢測結果(IE和其餘瀏覽器),來決定腳本處理過程當中監聽哪個事件。

實際上這裏的LoadScript()函數,就是咱們所說的LazyLoad.js(懶加載)的原型。

有了這個方法,咱們能夠實現一個簡單的多文件按某一固定順序加載代碼塊:

LoadScript('file-1.js', function(){
  LoadScript('file-2.js', function(){
    LoadScript('file-3.js', function(){
        console.log('loaded all')
    })
  })
})
複製代碼

以上代碼執行的時候,將會首先加載file-1.js,加載完成後再去加載file-2.js,以此類推。固然這種寫法確定是有待商榷的(多重回調嵌套寫法簡直就是地獄),但這種動態腳本添加的思想,和加載過程當中須要注意的和避免的問題,都在LoadScript函數中得以澄清解決。

固然,若是文件過多,而且加載的順序有要求,最好的解決方法仍是建議按照正確的順序合併一塊兒加載,這從各方面講都是更好的法子。

從加載方式上優化:XMLHttpRequest腳本注入

經過XMLHttpRequest對象來獲取腳本並注入到頁面也是實現無阻塞加載的另外一種方式,這個我以爲不難理解,這其實和動態添加腳本的方式是同樣的思想,來看具體代碼:

var xhr = new XMLHttpRequest();
xhr.open('get', 'file-1.js', true);
xhr.onreadystatechange = function() {
  if(xhr.readyState === 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
      // 若是從後臺或者緩存中拿到數據,則添加到script中並加載執行。
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.text = xhr.responseText;
      // 將建立的script添加到文檔頁面
      document.body.appendChild(script);
    }
  }
}

複製代碼

經過這種方式拿到的數據有兩個優勢:其一,咱們能夠控制腳本是否要當即執行,由於咱們知道新建立的script標籤只要添加到文檔界面中它就會當即執行,所以,在添加到文檔界面以前,也就是在appendChild()以前,咱們能夠根據本身實際的業務邏輯去實現需求,到了想要讓它執行的時候,再appendChild()便可。其二:它的兼容性很好,全部主流瀏覽器都支持,它不須要想動態添加腳本的方式那樣,咱們本身去寫特性檢測代碼;

但因爲是使用了XHR對象,因此不足之處是獲取這種資源有「域」的限制。資源 必須在同一個域下才能夠,不能夠跨域操做。

最後總結

文章主要從JavaScript的加載和執行這一過程當中挖掘探討對前端優化的解決方案,並較細緻的羅列了各個解決方案的優點和不足之處,固然,前端性能優化本就相對複雜,要想完全理解其各中起因,還有很長一段路要走!

本文主要行文思路:

  • 不得不說的JavaScript阻塞特性

  • 合理放置腳本位置,以優化加載體驗,js腳本放在 <body>標籤閉合以前。

  • 減小HTTP請求,壓縮精簡腳本代碼。

  • 無阻塞加載JavaScript腳本:

    • 使用<script>標籤的defer屬性。

    • 使用HTML5的async屬性。

    • 動態建立<script>元素加載JavaScript。

    • 使用XHR對象加載JavaScript。

最後,因爲我的水平緣由,如有行文不全或疏漏錯誤之處,懇請各位讀者批評指正,一路有你,不勝感激!。

感謝這個時代,讓咱們能夠站在巨人的肩膀上,窺探程序世界的宏偉壯觀,我願以一顆赤子心,踏遍程序世界的千山萬水!願每個行走在程序世界的同仁,都活成心中想要的樣子,加油!

相關文章
相關標籤/搜索