前言
關於JavaScript腳本載入的問題,相信大家碰到很多。主要在幾個點-
1> 同步腳本與非同步腳本帶來的檔案載入、檔案依賴及執行順序問題
2> 同步腳本與非同步腳本帶來的效能最佳化問題
深入理解腳本載入相關的方方面面問題,不僅利於解決實際問題,更有利於對效能最佳化的掌握並執行。
先看隨便一個script標籤程式碼——
如果放在
其中,test.js中的內容—
我們會看到,alert是一個暫停點,此時,頁面是空白的。但要注意,此時整個頁面已經載入完畢,如果body中包含某些src屬性的標籤(如上面的img標籤),此時瀏覽器已經開始載入相關內容了。總之要注意-js引擎和渲染引擎的工作時機是互斥的(一些書上叫它為UI線程)。
因此,我們需要——那些負責讓頁面更好看、更好用的腳本應該立即加載,而那些可以待會兒再加載的腳本稍後再加載。
一、腳本延遲執行
現在越來越流行把腳本放在頁面
標籤的尾部。這樣,一方面使用者可以更快看到頁面,另一方面腳本可以直接操作已經載入完成的dom元素。對於大多數腳本而言,這次「搬家」是個巨大的進步。頁面模型如下—
這確實大大加快了頁面的渲染時間,但是注意一點,這可能讓使用者有機會在載入bodyScript之前與頁面互動。源自於瀏覽器在載入完整個文件之前無法載入這些腳本,這對那些透過慢速連線傳送的大型文件來說會是一大瓶頸。
理想情況下,腳本的載入應該與文件的載入同時進行,並且不影響DOM的渲染。這樣,一旦文件就緒就可以運行腳本,因為已經按照<script>標籤的順序載入了對應腳本。 </script>
我們使用defer便能夠完成這樣的需求,即--
新增defer屬性相當於告訴瀏覽器:請馬上開始載入這個腳本吧,但是,請等到文件就緒且先前所有具有defer屬性的腳本都結束運行之後再執行它。
這樣,在head標籤裡放入延遲腳本,技能帶來腳本置於body標籤時的所有好處,又能讓大文檔的載入速度大幅提升。此時的頁面模式是-
但是並非所有的瀏覽器都支援defer(對於某些modern瀏覽器,如果聲明defer,其內部腳本將不會執行document.write及DOM渲染操作。IE4 皆支援defer屬性)。這意味著,如果您想確保自己的延遲腳本能在文件載入後運行,就必須將所有延遲腳本的程式碼都封裝在諸如jQuery之$(document).ready之類的結構中。這是值得的,因為差不多97%的訪客都能享受到並行加載的好處,同時另外3%的訪客仍然能使用功能完整的JavaScript。
二、腳本的完全並行化
讓腳本的載入及執行再快一步,我不想等到defer腳本一個接著一個運行(defer讓我們想到一種靜靜等待文檔加載的有序排隊場景),更不想等到文檔就緒之後才運行這些腳本,我想要盡快加載並且盡快運行這些腳本。這裡也就想到了HTML5的async屬性,但要注意,它是一種混亂的無政府狀態。
例如,我們加載兩個完全不相干的第三方腳本,頁面沒有它們也運行得很好,而且也不在乎它們誰先運行誰後運行。因此,對這些第三方腳本使用async屬性,相當於一分錢沒花就提升了它們的運行速度。
async屬性是HTML5新增的。作用和defer類似,即允許在下載腳本的同時進行DOM的渲染。但它將在下載後儘快執行(即JS引擎空閒了立刻執行),不能保證腳本會按順序執行。它們將在onload 事件之前完成。
Firefox 3.6、Opera 10.5、IE 9 和 最新的Chrome 和 Safari 都支援 async 屬性。可以同時使用 async 和 defer,這樣IE 4之後的所有 IE 都支援異步加載,但是要注意,async會覆蓋掉defer。
那麼此時的頁面模型如下-
要注意這裡的執行順序——各個腳本檔案加載,接著執行headScript.js,緊接著在DOM渲染的同時會在後台加載defferedScript.js。接著在DOM渲染結束時將執行defferedScript.js和那兩個非同步腳本,要注意對於支援async屬性的瀏覽器而言,這兩個腳本將做無序運行。
三、可程式的腳本載入
儘管上面兩個腳本屬性的功能非常吸引人,但是由於相容性的問題,應用並不是很廣泛。故此,我們更多地使用腳本載入其他腳本。例如,我們只想給那些滿足一定條件的使用者載入某個腳本,也就是經常提到的「懶加載」。
在瀏覽器API層面,有兩種合理的方法來抓取並執行伺服器腳本-
1> 產生ajax請求並用eval函數處理回應
2> 插入DOM<script>標籤</script>
後一種方式比較好,因為瀏覽器會替我們操心生成HTTP請求這樣的事。再者,eval也有一些實際問題:洩漏作用域,調試搞得一團糟,而且還可能降低效能。因此,想要載入名為feture.js的腳本,我們應該使用類似下面的程式碼:
當然,我們要處理回呼監聽,HTML5規格定義了一個可以綁定回呼的onload屬性。
不過,IE8及較舊的版本並不支援onload,它們支援的是onreadystatechange。而且,對於錯誤處理仍然千奇百怪。在這裡,可以多參考一些流行的校本載入函式庫,如labjs、yepnope、requirejs等。
如下,自己封裝了一個簡易loadjs檔案-
// script 標籤,IE下有onreadystatechange事件, w3c標準有onload事件
// IE9 也支援 W3C標準的onload
var ua = navigator.userAgent,
ua_version;
// IE6/7/8
if (/MSIE ([^;] )/.test(ua)) {
ua_version = parseFloat(RegExp["$1"], 10);
if (ua_version
script.onreadystatechange = function(){
if (this.readyState == "loaded" ){
則為 callback();
}
}
} else {
script.onload = function(){
則為 callback();
};
}
} else {
script.onload = function(){
callback();
};
}
};
document.write を使用したスクリプトの非同期読み込みについてはここでは説明しません。ブラウザー間の違いが非常にわかりにくいため、これを行う人はほとんどいません。
Image オブジェクトを使用して js ファイルを非同期的にプリロードする場合、内部の js コードは実行されないことに注意してください。
最後に、requirejs の非同期読み込みスクリプトについて話しましょう。
requirejs は、ターゲット スクリプトが順番に実行されることを保証するものではなく、実行順序がそれぞれの依存関係要件を満たすことができることのみを保証します。これにより、すべてのスクリプトができるだけ早く並行してロードされ、依存関係トポロジに従って順序正しく実行されるようになります。
4. 概要
OK、この時点で、非同期ロード スクリプトの記述は終了です。ここで最適化シーケンスについてもう一度詳しく説明します—
1> 従来の方法では、スクリプト タグを使用して HTML ドキュメントに直接埋め込みます。
という 2 つの状況があります。a> head タグに埋め込みます。これにより、ドキュメント コンテンツ内の他の静的リソース ファイルの並列読み込みには影響しません。影響するのは、ドキュメント コンテンツのレンダリング、つまり DOM レンダリングです。この時点ではブロックされており、白い画面が表示されます。
b> body タグの下部に埋め込む - 白画面現象を回避するために、DOM レンダリングを優先してスクリプトを実行しましたが、問題が再発しました。まず最初の問題について説明します。DOM ドキュメントのコンテンツが比較的大きい場合、インタラクティブ イベントのバインドに遅延が発生し、エクスペリエンスが低下します。もちろん、必要に応じて重要なスクリプトを最初に実行させる必要があります。 2 番目の問題について話しましょう。スクリプト ファイルはボディの下部にあるため、これらのスクリプトの読み込みはヘッドのスクリプトに比べて遅れます。したがって、ボディの底面に関しては、最適化の終点ではありません。
c> defer 属性を追加します。スクリプトをできるだけ早く並行してロードしたいので、これらのスクリプトをヘッドに置きます。スクリプトはドキュメントの読み込みと同時に読み込まれる必要があり、DOM のレンダリングに影響を与えるべきではありません。こうすることで、ドキュメントの準備ができたらスクリプトを実行できます。したがって、defer のような属性があります。ただし、defer 属性をサポートしていないブラウザの場合は、jQuery などのコードを $(document).ready にカプセル化する必要があります。 defer 属性を持つすべてのスクリプトは出現順に順次実行されるため、厳密に同期されることに注意してください。
2> 前のポイントはすべて、スクリプトの同期実行に関するものです (これらのスクリプトの読み込みプロセスは並行して行われます。唯一の違いは、誰が最初にリクエストをトリガーし、誰が後でリクエストをトリガーするかです)。もちろん、ある時点で実行できるのは 1 つの js ファイルだけであることはわかっています。ここでの「並列」とは、js エンジンが有効である限り、最初にそれをロードした人がすぐに実行することを意味します。この時点ではアイドル状態です。ここでの最適化は 2 つのタイプに分けられます—
a> async 属性を追加すると、前述の最適化ポイントを確実に達成できますが、これには大きな制限があり、最も適切な例は、複数のサードパーティ スクリプトを導入することです。 deffer属性との組み合わせもありますが、これが本当に面倒です。もちろん、互換性の問題もあります。上記の 3 つの問題により、その用途は限定されます。 async を使用する場合は、依存関係に十分な注意を払ってください。
b> スクリプト読み込みスクリプト - 明らかに、「スクリプトの並列実行」の目的を達成するために使用します。同時に、スクリプトの依存関係の問題を制御するのにも便利なので、requirejs での js の非同期読み込みにインテリジェントな読み込み管理を使用します。
はい、ここに書きましょう。
ここでは、非同期読み込みスクリプトについてのみ説明します。もう 1 つのコンテンツがあります。これは、スタイル ファイルまたはその他の静的リソースの非同期読み込みです。つづく…