瀏覽器線程阻塞和無阻塞加載腳本的理解

一個頁面,從被請求訪問,到用戶能夠看到頁面、操做頁面,到最後頁面徹底加載完畢,中間須要經歷一個至關奇幻的過程,這個過程的速度被「web性能師」孜孜不倦、前赴後繼的優化。本文討論的是其中一個優化。node

瀏覽器線程和阻塞

雖然你們耳熟能詳的一句話是:web

JavaScript是單線程的。跨域

可是:瀏覽器

瀏覽器固然不是單線程的。性能優化

瀏覽器的多線程中,有的線程負責加載資源,有的線程負責執行腳本,有的線程負責渲染界面,有的線程負責輪詢、監聽用戶事件。網絡

這些線程,根據瀏覽器自身特色以及web標準等等,有的會被瀏覽器特地的阻塞。兩個很明顯的阻塞就是:腳本執行時對其餘線程的阻塞腳本加載時對其餘線程的阻塞多線程

這兩個阻塞發生在HTML頁面初次解析時,它們對性能的影響較大,緣由是:併發

document對象綁定了一個事件:DOMContentLoaded。這個事件會在DOM解析完成以後觸發。這個事件觸發以後(而不是window.load事件1),會進入異步事件驅動階段(另外一個線程控制)。也就是說,DOM解析工做不完成,用戶與頁面的不少(並非全部)事件交互就沒法進行。這時候瀏覽器的忙指示(那個頁面上方的煩人的旋轉的圓圈)不會消失。app

咱們先從執行腳本時的阻塞提及。異步

<!--more-->

執行腳本帶來的阻塞

衆所周知,瀏覽器中有兩個引擎——JavaScript引擎和渲染引擎,它們對應了瀏覽器的兩個線程。這兩個引擎各司其職:

  1. JavaScript引擎解析並執行JavaScript代碼。

  2. 渲染引擎對界面進行繪製或者重繪(對DOM的渲染)。

在瀏覽器取得HTML文檔並解析HTML的時候,瀏覽器會:

  1. 對DOM樹進行解析。解析<body>中標籤內容時,能造成渲染樹,並渲染其中的元素。這個是在渲染引擎中作的。

  2. 遇到<script>標籤時(注意解析HTML時遇到的script標籤),基於JavaScript可能會修改DOM的考慮,其中的內容將會在此時被執行(若是是外部JS文件的代碼,會先加載這個文件資源,這部分後面再表)。執行JavaScript代碼是在JavaScript引擎中作的。

因爲:

腳本執行和渲染DOM的併發可能會引起嚴重的衝突,

因此:

JavaScript引擎和渲染引擎所在的兩個線程被設計爲互斥的!

這就意味着:

在執行<script>中內容時,瀏覽器會切換到JavaScript引擎所在的線程,此時渲染引擎所在的線程會阻塞,故其後元素的解析和渲染會暫停。這時候若是腳本執行時間太長的話,不只後面的元素會一直看不到,對DOM的解析工做也會一直完不成。用戶會陷入焦急的等待中。

解決這個問題的一個經典思路,就是:

<script>放到緊跟</body>以前的位置。這樣就不會影響須要放到頁面上的UI元素的解析了。這樣的好處就是,用戶能即便看到頁面上的UI元素,而防止出現了瀏覽器白屏等現象。

加載資源帶來的阻塞

再來講說加載。

加載是瀏覽器從網絡中請求資源(好比圖片、樣式文件、靜態腳本等),將資源進行相應的處理。

與上述說的兩個線程不一樣,對資源進行加載的線程通常不會和上述兩個線程互斥。例如圖片資源就能夠並行下載,下載的最大並行數量與瀏覽器的配置有關係。(這裏還有一個知識點,下載的最大並行數指的是從一個主機上下載的最大並行數,若是從多個主機下載資源,這個數量會翻倍,可是因爲對DNS的解析也是一個性能優化的點,故而通常策略是:不該設置超過4個主機,最好只設置2個主機)。

不會互斥意味着:資源的加載能夠和UI渲染、重排,事件響應,或者JavaScript代碼的執行的併發進行。

可是操蛋的就是,若是瀏覽器解析DOM時須要下載腳本資源,那麼下載這個資源的線程就是阻塞其餘下載線程以及渲染線程,致使渲染速度變慢。

爲了解決這個問題,咱們依然是將<script>標籤放到最後。

可是假設該腳本下載的速度較慢,並且多個腳本非併發下載,而且假如多個<script>內腳本執行時間較長的話,DOM解析工做仍是會一直完不成。

故而咱們須要無阻塞加載腳本的技術。

無阻塞加載腳本之一——defer

將問題暴露出來以後,咱們能夠根據其阻塞的緣由反向想出解決思路。

咱們可否將腳本資源文件像圖片同樣併發的加載而不是讓渲染線程掛起等待?道理上是能夠的。之因此要讓它阻塞等待,是由於擔憂JavaScript腳本會修改DOM。因此若是咱們對其比較放心的話,是沒必要讓渲染線程等待的。

這就是原生的defer思路:爲<script>標籤配置一個defer屬性(這是個bool屬性,配置以後表示爲true),這代表咱們知道腳本內沒有相似document.write的方法,此時標籤內的腳本就會並行的加載,而且在DOM解析完畢以後(即document.readyState變爲inactive以後和上述的DOMContentLoaded事件觸發以前),將腳本執行。注意:不一樣腳本的執行順序,是按照不一樣腳本相應的<script>在HTML中出現的順序決定的

<script src='..' defer></script>

可是defer在不一樣瀏覽器中的支持程度不一樣。咱們目前還不能特別依賴它。

defer的另外一個缺點是,下載的腳本,其執行順序和<script>標籤在HTML中出現的順序是一致的(同步的),而且執行腳本時也會阻塞其餘線程(這個沒法優化)。

另外一個更加沒有獲得支持的屬性是async,它跟defer相似,可是它是異步的。比同步的defer更快一步。咱們在這裏不討論async爲何不能被支持的問題,可是咱們接下來的技術跟async的步驟是類似的。

無阻塞加載腳本之二——動態腳本元素

所謂的動態腳本元素,就是說<script>標籤不是寫死在HTML中的,而是由現有的腳本生成的。

爲何能夠作到這樣呢?由於<script>標籤也是DOM元素的一種,而JavaScript是能夠經過DOM API操做DOM的。

不一樣於靜態腳本元素的解析,動態腳本元素在下載的時候是不會阻塞渲染線程的,也就是實現了並行下載。

<script>
    
        var node = document.createElement('script');
        
        node.src = '...';
        
        document.head.appendChild(node);
        
 </script>

這個標籤能夠放到</body>以前。

document.head.appendChild代碼以後,因爲沒有觸發渲染樹的重繪,切換回的渲染線程會將剩下的DOM解析並渲染完畢。同時新插入的<script>中的資源也會併發的下載。

那麼腳本在何時執行呢?

答案是<script>中的資源下載完以後會立刻執行。可是因爲此時DOM已經解析完畢,而且進入異步事件階段,因此即便切換到JavaScript引擎所在的線程上執行腳本,用戶也不會感受明顯的UI阻塞。

因爲資源的大小不一樣,因此這些腳本的執行將會是異步的

因爲腳本的異步執行,那麼如何解決腳本之間先後依賴的問題呢?咱們天然就會想到回調函數。在腳本加載執行完畢後,會觸發該<script>元素的onload事件,咱們能夠將回調放到這個事件中處理。

無阻塞加載腳本之三——XMLHttpRequest

這種方法的侷限很明顯:沒法跨域

可是若是是非跨域的腳本,咱們可使用XMLHttpRequest請求,將腳本放到responseText中,而且將其放到生成的<script>中.

xhr.onreadystatechange = function(){

    if (xhr.readyState == 4){
    
        var node = document.createElement('script');
        
        node.text = xhr.responseText;
        
        document.head.appendChild(node);
    }
}

這種方法一樣能夠異步的加載並執行腳本。

結束

上述是我本身的微小的看法。若有錯誤,歡迎指正。


  1. 自信源於犀牛書:《JavaScript權威指南(中文第六版)》326頁,13章。
相關文章
相關標籤/搜索