當前位置:
首頁 > 知識 > 更快地構建 DOM:使用預解析,async,defer 以及 preload

更快地構建 DOM:使用預解析,async,defer 以及 preload

在 2017年,保證頁面快速載入的手段涵蓋了方方面面,從壓縮和資源優化,到緩存,CDN,代碼分割以及 tree shaking 等。 然而,即便你不熟悉上面的這些概念,或者你感到無從下手,你仍然可以通過幾個關鍵字以及精細的代碼結構使得你的頁面獲得巨大的性能提升。

新的 Web 標準使你能夠更快地載入關鍵資源,這個月晚些時候,Firefox 就會支持這個特性。同時在Firefox Nightly版本或者開發者版本上已經可以使用這些功能。與此同時,這也是回顧基本原理,深入了解 DOM 解析相關性能的一個好時機。

理解瀏覽器的內部機制是每個 web 開發者最強有力的工具。我們看看瀏覽器是如何解釋代碼以及如何使用推測解析(speculative parsing)來幫助頁面快速載入的。我們會分析和是如何生效的以及如何利用新的關鍵字。

構建模塊

HTML 描述了一個頁面的結構。為了理解 HTML,瀏覽器首先會將HTML轉換成其能夠理解的一種格式 –文檔對象模型(Document Object Model)或者簡稱為 DOM。 瀏覽器引擎有這麼一段特殊的代碼叫做解析器,用來將數據從一種格式轉換成另外一種格式。一個 HTML 解析器就能將數據從 HTML 轉換到 DOM。

在 HTML 當中,嵌套(nesting)定義了不同標籤的父子關係。在 DOM 當中,對象被關聯在樹(一種數據結構)中用於捕獲這些關係。每一個 HTML 標籤都對應著樹種的某個節點(DOM節點)。

瀏覽器一個比特一個比特地構建 DOM。一旦第一個代碼塊載入到瀏覽器當中,它就開始解析 HTML,添加節點到樹中。

DOM 扮演著兩種角色:它既是 HTML 文檔的對象表示,也充當著外界(比如JavaScript)和頁面交互的介面。 當你調用,返回的元素是一個 DOM 節點。每個 DOM 節點都有很多函數可以用來訪問和改變它,用戶可以看到相應的變化。

GIF/44K

頁面上的 CSS 樣式被映射到 CSSOM 上 –CSS 對象模型(CSS Object Model)。它就像 DOM,但是只針對於 CSS 而不是 HTML。不像 DOM,它不能增量地構建。因為 CSS 規則會相互覆蓋,所以瀏覽器引擎要進行複雜的計算來確定 CSS 代碼如何應用到 DOM 上。

關於標籤的歷史

當瀏覽器構建 DOM 的時候,如果在 HTML 中遇到了一個標籤,它必須立即執行。如果腳本是來自於外部的,那麼它必須首先下載腳本。

在過去,為了執行一個腳本,HTML 的解析必須暫停。只有在 JavaScript 引擎執行完代碼之後它才會重新開始解析。

那位為什麼解析必須要暫停呢?那是因為腳本可以改變 HTML以及它的產物 —— DOM。 腳本可以通過方法添加節點來改變 DOM 結構。為了改變 HTML,腳本可以使用臭名昭著的方法來添加內容。它之所以臭名昭著是因為它能以進一步影響 HTML 解析的方式來改變 HTML。比如,該方法可以插入一個打開的注釋標籤來使得剩餘的 HTML 都變得不合法。

GIF/12K

腳本還可以查詢關於 DOM 的一些東西,如果是在 DOM 還在在構建的時候,它可能會返回意外的結果。

是一個遺留的方法,它能夠以預料之外的方式破壞你的頁面,你應該避免使用它。處於這些原因,瀏覽器開發出了一些複雜的方法來應對腳本阻塞導致的性能問題,稍後我會解釋。

那麼 CSS 會阻塞頁面嗎 ?

JavaScript 阻塞頁面解析是因為它可以修改文檔。CSS 不能修改文檔,所以看起來它沒有理由去阻塞頁面解析,對嗎?

那麼,如果腳本需要樣式信息,但樣式還沒有被解析呢?瀏覽器並不知道腳本要怎麼執行——它可能會需要類似 DOM 節點的屬性,而這個屬性又依賴於樣式表,或者它期望能夠直接訪問 CSSOM。

正因為如此,CSS 可能會阻塞解析,取決於外部樣式表和腳本在文檔中的順序。如果在文檔中外部樣式表放置在腳本之前,DOM 對象和 CSSOM 對象的構建可以互相干擾。 當解析器獲取到一個 script 標籤,DOM 將無法繼續構建直到 JavaScript 執行完畢,而 JavaScript 在 CSS 下載完,解析完,並且 CSSOM 可以使用的時候,才能執行。

另外一件要注意的事是,即使 CSS 不阻塞 DOM 的構建,它也會阻塞 DOM 的渲染。直到 DOM 和 CSSOM 準備好之前,瀏覽器什麼都不會顯示。這是因為頁面沒有 CSS 通常無法使用。如果一個瀏覽器給你顯示了一個沒有 CSS 的凌亂的頁面,而幾分鐘之後又突然變成了一個有樣式的頁面,變換的內容和突然視覺變化使得用戶體驗變得非常糟糕。

具體可以參考由 Milica (@micikato) 在 CodePen 上製作的例子 —— Flash of Unstyled Content。

這種糟糕的用戶體驗有一個名字 — Flash of Unstyled Content 或是 FOUC

為了避免這個問題,你應該儘快地呈現 CSS。記得流行的「樣式放頂部,腳本放底部」的最佳實踐嗎?你現在知道它是怎麼來的了!

回到未來 – 預解析(speculative parsing)

每當解析器遇到一個腳本就暫停意味著每個你載入的腳本都會推遲發現鏈接到 HTML 的其他資源。

如果你有幾個類似的腳本和圖片要載入,例如:

這個過程過去是這樣的:

這個狀況在 2008 年左右改變了,當時 IE 引入了一個概念叫做 「先行下載」。 這是一種在同步的腳步執行的時候保持文件的下載的一種方法。Firefox,Chrome 和 Safari 隨後效仿,如今大多數的瀏覽器都使用了這個技術,它們有著不同的名稱。Chrome 和 Safari 稱它為 「預掃描器」 而 Firefox 稱它為預解析器。

它的概念是:雖然在執行腳本時構建 DOM 是不安全的,但是你仍然可以解析 HTML 來查看其它需要檢索的資源。找到的文件會被添加到一個列表裡並開始在後台並行地下載。當腳本執行完畢之後,這些文件很可能已經下載完成了。

上面例子的瀑布圖現在看起來是這樣的:

以這種方式觸發的下載請求稱之為 「預測」,因為很有可能腳本還是會改變 HTML 結構(還記得嗎?),導致了預測的浪費。雖然這是有可能的,但是卻不常見,所以這就是為什麼預解析仍然能夠帶來很大的性能提升。

而且其他瀏覽器只會對鏈接的資源進行這樣的預載入。在 Firefox 中,HTML 解析器對 DOM 樹的構建也是演算法預測的。有利的一面是,當推測成功的時候,就沒有必要重新解析文件的一部分了。缺點是,如果推測失敗了,就需要更多的工作。

關於(預)載入

這種資源載入的方式帶來了顯著地性能提升,你不需要做任何事情就可以使用這種優勢。然而,作為一個 web 開發者,了解預解析是如何工作的能幫你最大程度地利用它。

可以預載入的東西在瀏覽器之間有所不同,但所有的主要的瀏覽器都會預載入:

腳本

外部 CSS

來自標籤的圖片

Firefox 也會預載入 video 元素的屬性,而 Chrome 和 Safari 會預載入規則的內聯樣式。

瀏覽器能夠並行下載的文件的數量是有限制的。這個限制在不同瀏覽器之間是不同的,並且取決於不同的因素,比如:你是否從同一個伺服器或是不同的伺服器下載所有的文件,又或者是你使用的是 HTTP/1.1 或是 HTTP/2 協議。為了更快地渲染頁面,瀏覽器對每個要下載的文件都設置優先順序來優化下載。為了弄清這些的優先順序,他們遵守基於資源類型、標記位置以及頁面渲染的進度的複雜方案。

在進行預解析時,瀏覽不會執行內聯的 JavaScript 代碼塊。這意味著它不會發現任何的腳本注入資源,這些資源會排到抓取隊列的最後面。

你應該儘可能使瀏覽器能更輕鬆訪問到重要的資源。你可以把他們放到 HTML 標籤當中或者將要載入的腳本內聯到文檔的前面。然而,有時候需要一些不重要的資源晚一點被載入。這種情況,你通過 JavaScript 來載入他們來避免預解析。

你也可以看看這個MDN 指南,裡面講述了如何針對預解析優化你的頁面。

defer 和 async

不過,同步的腳本阻塞解析器仍舊是個問題。並不是所有的腳本對用戶體驗都是同等的重要,例如那些用於監測和分析的腳本。解決方法呢?就是去儘可能地非同步載入這些不那麼重要的腳本。

和屬性提供給開發者一個方式來告訴瀏覽器哪些腳本是需要非同步載入的。

這兩個屬性都告訴瀏覽器,它可以 「在後台」 載入腳本的同時繼續解析 HTML,並在腳本載入完之後再執行。這樣,腳本下載就不會阻塞 DOM 構建和頁面渲染了。結果就是,用戶可以在所有的腳本載入完成之前就能看到頁面。

和之間的不同是他們開始執行腳本的時機的不同。

比要先引入瀏覽器。它的執行在解析完全完成之後才開始,它處在事件之前。 它保證腳本會按照它在 HTML 中出現的順序執行,並且不會阻塞解析。

腳本在它們完成下載完成後的第一時間執行,它處在 window 的事件之前。 這意味著有可能(並且很有可能)設置了 async 的腳本不會按照它們在 HTML 中出現的順序執行。這也意味著他們可能會中斷 DOM 的構建。

無論它們在何處被指定,設置的腳本的載入有著較低的優先順序。他們通常在所有其他腳本載入之後才載入,而不阻塞 DOM 構建。然而,如果一個指定的腳本很快就完成了下載,那麼它的執行會阻塞 DOM 構建以及所有在之後才完成下載的同步腳。

注: async 和 defer 屬性只對外部腳本起作用,如果沒有屬性它們會被忽略。

preload

如果你想要延遲處理一些腳本,那麼和非常棒。那網頁上那些對用戶體驗至關重要的東西呢?預解析器很方便,但是它們只會預載入少數類型的資源並遵循其邏輯。通常的目的都是首先交付 CSS,因為它會阻塞渲染。同步的腳本總是比非同步的腳本擁有更高的優先順序。視口中可見的圖像會比那些底下的圖片先下載完。還有字體,視頻,SVG… 總而言之 — 這個過程很複雜。

作為作者,你知道哪些資源對你的頁面渲染來說是最重要的。它們其中一些經常深藏在 CSS 或者是腳本當中,甚至瀏覽器需要花上很長一段時間才會發現他們。對於那些重要的資源,你現在可以使用來告訴瀏覽器你需要儘快地載入它們。

你只需要寫上:

你幾乎可以鏈接到任何東西上,屬性告訴瀏覽器要下載的是什麼。一些可能的值是:

你可以在MDN上查看剩餘的內容類型。

字體可能是隱藏在CSS中最重要的東西。它們對頁面上文字的渲染非常地關鍵,但是它們知道瀏覽器確認它們會被使用之前都不會被載入。 這個檢查只發生在 CSS 已經被解析,應用,並且瀏覽器已經將 CSS 規則匹配到對應的 DOM 節點上時。這個過程在頁面載入的過程中發生的相當晚,並且常常導致文字渲染中不必要的延遲。你可以通過使用 preload 屬性來避免。

有一點要注意,要預載入字體你還必須設置crossorigin屬性,即使字體在同一個域名下:

preload 特性目前只有有限的支持度,因為其他瀏覽器還在推出它的過程中。你可以在這裡查看進度。

結論

瀏覽器是自 90 年代以來一直在進化的極其複雜的野獸。我們已經討論了一些遺留問題以及 Web 開發中的一些最新標準。根據這些指南書寫你的代碼能夠幫助你選擇最好的策略來提供更加流暢的瀏覽器體驗。

原文:https://hacks.mozilla.org/2017/09/building-the-dom-faster-speculative-parsing-async-defer-and-preload

譯者:Mactavish

譯文:http://zcfy.cc/article/4224

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 JavaScript 的精彩文章:

你是幾號?
程序員在國慶節如何假裝旅遊?
國慶加班是一種什麼樣的體驗?
Visual Studio Code 現支持深度學習/AI 應用程序
騰訊面試經驗

TAG:JavaScript |