一、阻塞特性 |
《高性能JavaScript》一書中,關於第一章“Loading and Execution”,提到了無阻塞加載JavaScript技術,目的是為了提高頁面呈現速度。
說到無阻塞加載JavaScript要點,我們就有必要知道,為什么在html中不管是內聯JavaScript還是外聯,會影響到頁面的性能?
原因是:JavaScript是單線程,在JavaScript運行時其他的事情不能被瀏覽器處理。事實上,大多數瀏覽器使用單線程處理UI更新和JavaScript運行等多個任務,而同一時間只能有一個任務被執行。所以在執行JavaScript時,會妨礙其他頁面動作。這是JavaScript的特性,我們沒法改變。
並且,html解析過程是至上而下的,當html解析器遇到諸如<script>、<link>等標簽時,解析器就會停止下來,去下載相應的內容。需要注意的是,在加載<script>、<link>標簽時都會阻止解析器往下執行。
並且,html解析過程是至上而下的,當html解析器遇到諸如<script>、<link>等標簽時,就會去下載相應內容。且加載、解析、執行JavaScript會阻止解析器往下執行。
那什么時候,html解析器才能往下繼續解析html文檔呢?
就JavaScript而言,當html解析器遇到<script>標簽,無論它是內聯還是外聯,頁面中的下載和解析過程都必須停止,直到<script>從外部加載進來的JavaScript或內聯的JavaScript運行完畢,方可繼續解析。在高版本的瀏覽器當中,允許並行下載JavaScript文件,當一個<script>標簽正在下載外部資源時,不必阻塞其他<script>標簽,但是不幸地是,JavaScript的下載仍然會阻塞其他資源的下載,例如圖片。這里還需要值得注意的是,對於樣式和腳本的先后順序同樣會影響到瀏覽器的解析過程,比如將<link>標簽放在<script>標簽前面,如果樣式下載受阻,那么將阻塞<link>后面的<script>加載和執行,究其原因主要在於:script腳本在執行過程中可能會引用到相關樣式。
了解了JavaScript在html中的阻塞特性,我們再來看看如何改善其阻塞特性。
二、改善方法 |
--最簡單做法--:
為了讓html文檔在解析時,盡量地快,常規的做法是將<script>標簽放到</body>標簽的前面,這樣就不會阻塞html中其他資源的下載了。
如下:
盡管腳本下載之間互相阻塞,但頁面已經下載完成並且顯示在用戶面前了,進入頁面的速度不會顯得太慢。且,為了讓腳本之間的互相阻塞最小化,通常將多個相關的JavaScript文件合並為一個JavaScript文件,另外這樣做帶來的好處不僅讓腳本之間阻塞變小,還減少了http請求的數量。
但,這樣做JavaScript文件下載之間還是會阻塞,特別是當JavaScript文件逐漸變多時。
故而,引入無阻塞腳本技術。
無阻塞腳本技術主要分為兩大類:
1、 HTML5中的defer和async;
2、 動態創建script為dom元素。
下面將分別介紹。
--HTML5中的defer和async--:
HTML5中提供了兩個屬性供<script>標簽使用,目的就是為了無阻塞加載JavaScript。
用法如下:
<script src="file1.js" defer></script> <script src="file2.js" async></script>
需要注意的是,這兩個屬性對內聯JavaScript是無效的,只針對外聯JavaScript,如上所示。
加載流程:
當解析器遇到設置defer或者async屬性的<script>元素時,它開始下載腳本,並繼續解析文檔。腳本會在它下載完成后盡快執行,但是解析器沒有停下來等待他下載。
defer和async區別:
就defer和async的區別而言,使用defer的<script>標簽是按照他們排列的順序執行的,而使用async的<script>標簽是不按他們在HTML中的排列順序執行的;
就執行時間而言,defer是在DOMContentloaded事件之前執行,而async是在window.onload事件之前執行的,且只支持IE10+。當defer和async同時存在時,會忽略defer而遵循async。且使用defer和async的腳本禁止使用document.write方法哦。
--動態腳本元素--:
因為script標簽是在html中的,是屬於dom元素,所以我們完全可以利用dom方法創建一個動態的script元素。
如下:
var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'file1.js'; document.getElementsByTagName('head')[0].appendChild(script);
“當創建的script元素添加到頁面后立刻開始下載。此技術的重點在於:無論在何處啟動下載,文件的下載和運行都不會阻塞其他頁面的處理過程。你甚至可以將這些代碼放在<head>部分而不會對其余部分的頁面代碼造成影響(除了用於下載文件的HTTP連接)”
上面加粗部分引至《高性能JavaScript》,當時在我讀到這句話時,不是很理解,在前面“阻塞特性”一小節中,我們提到JavaScript是單線程且與UI線程互排,那么JavaScript在運行時,怎么不會阻塞其他頁面的處理過程呢?
為此,帶着這一困惑在博客園問答中心提出了自己的觀點並與道友討論(‘博問點擊此’)。
通過與道友討論以及自己查看了相關文檔后,有了自己見解:
之所以動態創建script元素去加載JavaScript文件,不會對頁面其余操作影響,原因如下:
1、html解析器將script當做了dom元素,而不是script標簽,所以就不對其進行諸如加載、解析、運行時,停止頁面中一切行為。打了個擦邊球。
2、JavaScript是單線程,且與UI線程共享同一個線程,但這不代表瀏覽器就只有一個線程。所以在執行JavaScript代碼時,不影響圖片之類的下載。
好了,回到剛才采用動態腳本元素的方法,我們還得完善下,原因是上述代碼,在‘自運行’時還好,但是如果引用了其他js文件中的方法呢?那就得出錯咯。因為我們無法保證動態腳本元素執行JavaScript代碼的順序。針對這一問題,標准瀏覽器我們可以利用<script>節點的load事件處理,而IE瀏覽器我們可以利用其特有的readystatechange事件處理。
封裝好的代碼如下:
function loadScript(url, callback){ var script = document.createElement('script'); script.type = 'text/javascript'; /* 在IE中readyState值所表示的最終狀態並不一致, 有時<script>元素會得到"loaded"卻不出現"complete", 但另外一些情況下出現"complete"而用不到"loaded"。 最安全的辦法就是在readystatechange事件中檢查這兩種狀態, 並且當其中一種狀態出現時,刪除readystatechange事件句柄(保證事件不會被觸發兩次) */ if(script.readyState){//IE script.onreadystatechange = function(){ if(script.readyState == 'loaded' || script.readyState == 'complete'){ script.onreadystatechange = null; callback() } } }else{//Other script.onload = function(){ callback(); } } script.src = url; document.getElementsByTagName('head')[0].appendChild(script); }
所以,當頁面中動態加載多個有關聯的JavaScript文件時,我們可以將其串聯起來,保證順序。
如下:
//串聯起來 loadScript('file1.js',function(){ loadScript('file2.js',function(){ ... }); });
除開這種方法,還有一種就是“XHR腳本注入”,大體內容與上面的方法差不多,都需要動態創建script元素,區別在於該方法利用XMLHttpRequest對象,請求JavaScript文件,並將請求到的responseText,插入script元素的text中。因為是借助XMLHttpRequest對象,缺點顯而易見,不能跨域請求。
示例代碼如下:
var xhr = new XMLHttpRequest(); xhr.open('get', 'file1.js', true); xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if(xhr.status >= 200 && xhr.status < 300 || xhr.status ==304){ var script = document.createElement('script'); script.type = 'text/javascript'; script.text = xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null);
三、拓展閱讀 |
[2] HTML渲染過程詳解
[3] 瀏覽器加載渲染網頁過程解析
[4] defer、async屬性以及JS異步加載並執行解決方案