CSS 與網(wǎng)絡(luò)性能

原文鏈接:CSS and Network Performance

昨天看到的一篇比較全面地介紹了 CSS 加載的相關(guān)知識的文章,文章挺長,原文是全英文的。由于譯者水平有限,有能力的同學(xué)建議直接看原文,同時也希望譯文對你有所幫助,謝謝~以下是正文:


承蒙抬愛,我被稱為 CSS 魔術(shù)師已經(jīng)十多年了,但最近在博客上,CSS 相關(guān)的文章卻不多。那就結(jié)合 CSS 與性能這兩大主題,為大家?guī)硪黄恼掳伞?/p>

CSS 是頁面渲染的關(guān)鍵因素之一,(當(dāng)頁面存在外鏈 CSS 時,)瀏覽器會等待全部的 CSS 下載及解析完成后再渲染頁面。關(guān)鍵路徑上的任何延遲都會影響首屏?xí)r間,因而我們需要盡快地將 CSS 傳輸?shù)接脩舻脑O(shè)備,否則,(在頁面渲染之前,)用戶只能看到一個空白的屏幕。

最大的問題是什么?


廣義而言,CSS 是(渲染)性能的關(guān)鍵,這是由于:

  1. 瀏覽器直到渲染樹構(gòu)建完成后才會渲染頁面;
  2. 渲染樹由 DOM 與 CSSOM 組合而成;
  3. DOM 是 HTML 加上(同步)阻塞的 JavaScript 操作(DOM 后的)結(jié)果;
  4. CSSOM 是 CSS 規(guī)則應(yīng)用于 DOM 后的結(jié)果;
  5. 使 JavaScript 非阻塞非常簡單,添加 async 或 defer 屬性即可;
  6. 相對而言,要讓 CSS 變?yōu)楫惒郊虞d是比較困難的;
  7. 所以記住這條經(jīng)驗(yàn)法則:(理想情況下,)最慢樣式表的下載時間決定了頁面渲染的時間

基于上述考慮,我們需要盡快構(gòu)建 DOM 與 CSSOM。一般情況下,DOM 的構(gòu)建是相對較快,(當(dāng)請求某個頁面時,)服務(wù)器響應(yīng)的首個請求是 HTML 文檔。但一般 CSS 是作為 HTML 的子資源而存在,因此 CSSOM 的構(gòu)建通常需要更長的時間。

在這篇文章中,會講述 CSS 為何是網(wǎng)絡(luò)瓶頸(無論是對于它自己或是其他資源),該如何突破它,從而縮短關(guān)鍵路徑以減少首次渲染前的等待時間。

使用關(guān)鍵 CSS


如果條件允許,縮短渲染前等待時間最有效的方式就是使用 Critical CSS (關(guān)鍵 CSS)模式:找出首次渲染所需的樣式(通常是首屏相關(guān)的樣式),將它們內(nèi)聯(lián)到 <head> 標(biāo)簽中,其他樣式則通過異步的方式進(jìn)行加載。

雖然這十分有效,但實(shí)施起來卻并不容易,比如:高度動態(tài)化的網(wǎng)站(譯者注:如 SPA)通常難以提取首屏相關(guān)的樣式、提取的過程需要自動化、需要對首屏不同元素顯示或隱藏的狀態(tài)作出假設(shè)、某些邊界情況難以處理以及相關(guān)工具仍未成熟等問題。如果你的項(xiàng)目相當(dāng)龐大或是有歷史包袱,這將變得更為復(fù)雜。

根據(jù)媒體類型拆分代碼


如果在項(xiàng)目組難以執(zhí)行關(guān)鍵 CSS 策略,可以嘗試根據(jù)媒體查詢拆分 CSS 文件,這也是一種可靠的策略。執(zhí)行此策略后,瀏覽器表現(xiàn)如下:

  • 以非常高的優(yōu)先級下載符合當(dāng)前上下文(設(shè)備、屏幕尺寸、分辨率、方向等)的 CSS 文件,阻塞關(guān)鍵路徑;
  • 以非常低的優(yōu)先級下載不符合當(dāng)前上下文的 CSS 文件,不會阻塞關(guān)鍵路徑。

瀏覽器基本上能將未命中媒體查詢的 CSS 文件延遲下載。

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="all.css" />
復(fù)制代碼
</pre>

如果我們把全部的 CSS 代碼都放在一個文件中,請求的表現(xiàn)如下:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

我們可以觀察到,這個單獨(dú)的 CSS 文件會以 最高 的優(yōu)先級下載。

根據(jù)媒體查詢拆分成若干個 CSS 文件后:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="all.css" media="all" />
<link rel="stylesheet" href="small.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="medium.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="large.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="extra-large.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />
復(fù)制代碼
</pre>

瀏覽器會以不同的優(yōu)先級下載 CSS 文件:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

瀏覽器仍然會下載全部的 CSS 文件,但只有符合當(dāng)前上下文的 CSS 文件會阻塞渲染。

避免在 CSS 文件中使用 @import


為縮短渲染等待時間而努力的下一項(xiàng)任務(wù)非常簡單:避免在 CSS 文件中使用 @import

如果了解 @import 的原理,那應(yīng)該清楚它的性能并不高,使用它會阻塞渲染更長時間。這是因?yàn)槲覀冊陉P(guān)鍵路徑上創(chuàng)造了更多(隊(duì)列式)的網(wǎng)絡(luò)請求:

  1. 下載 HTML;
  2. 請求并下載依賴的 CSS;
  • (下載及解析完成后,本該是構(gòu)造渲染樹,然而;)
  1. CSS 依賴了其他的 CSS,繼續(xù)請求并下載 CSS 文件;
  2. 構(gòu)造渲染樹。

以下是相關(guān)的案例:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="all.css" />
復(fù)制代碼
</pre>

all.css 的內(nèi)容:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@import url(imported.css);
復(fù)制代碼
</pre>

最終,瀏覽器的請求瀑布圖呈現(xiàn)為:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

關(guān)鍵路徑上的 CSS 文件并沒有并行下載。

通過將 @imports 請求的文件改為 <link rel="stylesheet" />:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="all.css" />
<link rel="stylesheet" href="imported.css" />
復(fù)制代碼
</pre>

可以提高網(wǎng)絡(luò)性能:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

關(guān)鍵路徑上的 CSS 文件是并行下載的。

注意,有一個特殊的情況值得討論。如果你沒有包含 @import 的 CSS 文件的修改權(quán)限,為了讓瀏覽器并行下載 CSS 文件,可以往 HTML 中補(bǔ)充相應(yīng)的 <link rel="stylesheet" src="@import的地址" />。瀏覽器會并行下載相應(yīng)的 CSS 文件且不會重復(fù)下載 @import 引用的文件。

在 HTML 中謹(jǐn)慎地使用 @import


本節(jié)的內(nèi)容比較奇怪。各大瀏覽器的相關(guān)實(shí)現(xiàn)上似乎都有問題,我以前提交了相關(guān)的bugs(譯者注:簡單說,當(dāng)頁面中存在:<style>@import url(xxx.url);</style>,瀏覽器不會并行下載,但加上引號后:<style>@import url("xxx.url");</style>,瀏覽器會并行下載)。

為了透徹地理解本節(jié)的內(nèi)容,首先我們需要了解瀏覽器的預(yù)加載掃描器:各大瀏覽器都實(shí)現(xiàn)了一個名為預(yù)加載掃描器的輔助解析器。瀏覽器的核心解析器主要用于構(gòu)建 DOM、CSSOM、運(yùn)行 JavaScript 等。HTML 文檔中某些標(biāo)簽與狀態(tài)會阻塞核心解析器,因而核心解析器的運(yùn)行是斷斷續(xù)續(xù)的。而預(yù)加載掃描器可以跳到核心解析器尚未解析的部分,用以發(fā)現(xiàn)其他待引用的子資源(如 CSS、JS 文件、圖片等)。一旦發(fā)現(xiàn)此類子資源,預(yù)加載掃描器會開始下載它們,以便核心解析器在解析到對應(yīng)內(nèi)容時就能使用它們(,而不是直到那一刻才開始下載該資源)。預(yù)加載掃描器的出現(xiàn),使網(wǎng)頁的加載性能提高了19%,這是一項(xiàng)了不起的成就,可以極大地優(yōu)化用戶體驗(yàn)。

作為開發(fā)者,需要警惕預(yù)加載掃描器背后隱藏的問題,這在后文會進(jìn)行闡述。

在 HTML 中使用 @import,在以 WebKit 與 Blink 為內(nèi)核的瀏覽器中,可能會觸發(fā)它們預(yù)加載掃描器的 bug,在 Firefox 與 IE/Edge 中,則表現(xiàn)低效。

Firefox 與 IE / Edge:在 HTML 中將 @import 放在 JS 和 CSS 之前

在 Firefox 與 IE/Edge 中,預(yù)加載掃描器不會并行下載 <script src=""> 和 <link rel="stylesheet" /> 后 @imports 引用的資源。

這意味著如下的 HTML:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><script src="app.js"></script>
<style>
@import url(app.css);
</style>
復(fù)制代碼
</pre>

會出現(xiàn)這樣的請求瀑布圖:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

由于預(yù)加載掃描器失效,導(dǎo)致資源在 Firefox 中無法并行下載(IE/Edge 中有著同樣的問題)。

通過上圖,可以清晰地觀察到:直到 JavaScript 文件下載完成之后,@import 引用的 CSS 文件才開始下載。

不單 <script> 標(biāo)簽會觸發(fā)此問題,<link> 標(biāo)簽也會:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="style.css" />
<style>
@import url(app.css);
</style>
復(fù)制代碼
</pre>

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

與 <script> 標(biāo)簽一樣,子資源無法并行下載。

此問題最簡單的解決方案是調(diào)換 <script> 或 <link rel="stylesheet" /> 標(biāo)簽與(包含 @import 的)<style> 標(biāo)簽的位置。然而,當(dāng)我們改變順序時,可能會對頁面造成影響。

最佳解決方案是完全不使用 @import,再往 HTML 文檔中加入另一個 <link rel="stylesheet" /> 取而代之:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="app.css" />
復(fù)制代碼
</pre>

修改后,瀏覽器表現(xiàn)更好:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

瀏覽器并行下載資源,IE/Edge 表現(xiàn)相同。

以 Blink 或 WebKit 內(nèi)核的瀏覽器:在 HTML 文檔中使用 @import 時,要用引號包裹 url。

對于以 Blink 或 WebKit 為內(nèi)核的瀏覽器而言,當(dāng) @import 引用的 url 未被引號包裹時,表現(xiàn)與 Firefox 和 IE/Edge 一致(無法并行下載)。這意味著上述兩個內(nèi)核的預(yù)加載掃描器存在 bug。

因此,無需調(diào)整代碼的順序,只需要添加引號即可解決問題。但我還是建議使用另一個 <link rel="stylesheet" /> 取代 @import。

未添加引號時的代碼:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="style.css" />
<style>
@import url(app.css);
</style>
復(fù)制代碼
</pre>

瀑布圖:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

可以看到,缺失引號會破壞 Chrome 的預(yù)加載(Opera 與 Safari 表現(xiàn)也是如此。)

添加引號后的代碼:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="style.css" />
<style>
@import url("app.css");
</style>
復(fù)制代碼
</pre>

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

添加引號后,Chrome、Opera 和 Safari 的預(yù)加載掃描器表現(xiàn)恢復(fù)正常,

這絕對是 WebKit 與 Blink 內(nèi)核的一個 bug,是否添加引號不應(yīng)成為影響預(yù)加載掃描器的因素。

感謝 Yoav 幫我追蹤這個問題。

現(xiàn)在這個 bug 現(xiàn)已在 Chromium 的待修復(fù)列表中。

不要將動態(tài)插入 JavaScript 的代碼放在 <link rel="stylesheet" /> 之后


在上一節(jié)中,我們了解到某些引用 CSS 文件路徑 的方法,會對其他資源的下載造成負(fù)面影響。在本節(jié)中,我們將探究為何稍有不慎,CSS 將延遲其他資源的下載。該問題主要出現(xiàn)在動態(tài)創(chuàng)建的 <script> 標(biāo)簽中:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
復(fù)制代碼
</pre>

所有瀏覽器都存在一個鮮為人知,但符合邏輯的現(xiàn)象,它會對性能造成很大的影響:

在瀏覽器下載完該 CSS 文件之前,不會執(zhí)行下面的 JS

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="slow-loading-stylesheet.css" />
<script>
console.log("I will not run until slow-loading-stylesheet.css is downloaded.");
</script>
復(fù)制代碼
</pre>

這是合理的。當(dāng) CSS 文件尚未下載完成時,HTML 文檔中任何同步的 JavaScript 代碼,均不會執(zhí)行。考慮以下場景: <script> 中的代碼會訪問當(dāng)前的頁面樣式,為確保結(jié)果正確,需要等待( <script> 標(biāo)簽前)所有 CSS 文件下載并解析完畢后再獲取,否則無法保證正確性。因此,在 CSSOM 構(gòu)建完成之前,<script> 中的代碼不會執(zhí)行。

根據(jù)這現(xiàn)象,CSS 文件的下載時間會對后續(xù) <script> 的執(zhí)行時間造成影響。下面的例子能較好地說明問題。

如果我們將一個 <link rel="stylesheet" /> 放在 <script> 之前,<script> 中動態(tài)創(chuàng)建新 <script> 的代碼只會在 CSS 文件下載完之后才會執(zhí)行,這意味著 CSS 推遲了資源的下載與執(zhí)行:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><link rel="stylesheet" href="app.css" />
<script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
復(fù)制代碼
</pre>

從下面的瀑布圖可以看到,JavaScript 文件在 CSSOM 構(gòu)建完成之后才開始下載,完全失去了并行下載的優(yōu)勢:

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

盡管預(yù)加載掃描器希望能預(yù)下載 analytics.js,但對 analytics.js 的引用并非一開始就存在于 HTML 的文檔之中,它是由 <link> 后面 <script> 的代碼動態(tài)創(chuàng)建的,在創(chuàng)建之前,它只是一些字符串,而不是預(yù)加載掃描器可識別的資源,無形中它被隱藏起來了。

為了更安全地加載腳本,第三方服務(wù)商經(jīng)常提供這樣的代碼片段。然而,開發(fā)者通常不信任第三方的代碼,因而會把該片段放在頁面的最后,但這可能會導(dǎo)致不良的后果。事實(shí)上,Google Analytics (在文檔中)對此的建議是:

將代碼復(fù)制后,作為第一項(xiàng)粘貼到待追蹤頁面的 中。

綜上,我的建議是:

如果 <script> 中的代碼并不依賴 CSS,把它們放在樣式表之前。

調(diào)整一下代碼:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
<link rel="stylesheet" href="app.css" />
復(fù)制代碼
</pre>

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

交換位置之后,子資源可以并行下載,頁面的整體性能提高了兩倍以上。(譯者注:本節(jié)的內(nèi)容只同意一半,<head> 中的代碼,確實(shí)是建議先放 <script>,再放 <link>,后文也會有相關(guān)的內(nèi)容,但第三方代碼放在 <head> 中的第一項(xiàng),取決于相關(guān)代碼的用途。如非必要,放在頁面末尾或空閑時下載及執(zhí)行也未嘗不可)

將無需查詢 CSSOM 的 JavaScript 代碼放在 CSS 文件之前,需要查詢的放在 CSS 文件之后

這條建議遠(yuǎn)比你想象中的有用。

上文討論了插入新 <script> 的代碼應(yīng)放在 <link> 之前,那是否能推廣到其他的 CSS 與 JavaScript 呢?為了弄明白這個問題,先提出以下假設(shè):

假設(shè):

  • CSSOM 的構(gòu)建會阻塞 CSS 后面同步 JS 的執(zhí)行;
  • 同步的 JS 會阻塞 DOM 的構(gòu)建...

那如果 JS 并不依賴 CSSOM,以下那種情況會更快?

  • script 在前 style 在后;
  • style 在前 script 在后?

答案是:

如果 JS 文件沒有依賴 CSS,你應(yīng)該將 JS 代碼放在樣式表之前。 既然沒有依賴,那就沒有任何理由阻塞 JavaScript 代碼的執(zhí)行。

(盡管執(zhí)行 JavaScript 代碼時會停止解析 DOM, 但預(yù)加載掃描器會提前下載之后的 CSS)

如果你一部分 JavaScript 需要依賴 CSS 而另一部分卻不用,最佳的實(shí)踐是將 JavaScript 分為兩部分,分別置于 CSS 的兩側(cè):

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
<script src="i-need-to-block-dom-but-DONT-need-to-query-cssom.js"></script>
<link rel="stylesheet" href="app.css" />

<script src="i-need-to-block-dom-but-DO-need-to-query-cssom.js"></script>
復(fù)制代碼
</pre>

根據(jù)這種組織方式,我們的頁面會按最佳的方式下載與執(zhí)行相關(guān)代碼。下面的截圖中,粉色代表 JS 的執(zhí)行,但它們都比較“纖細(xì)”了,希望你能看得清楚。(第一欄的(下同))第一行是整個頁面的時間軸,留意該行粉色的部分,代表 JS 正在執(zhí)行。第二行是首個 JS 文件的時間軸,可以看到下載完后并立即執(zhí)行。第三行是 CSS 的時間軸,因而沒有任何 JS 執(zhí)行。最后一行是第二個 JS 文件的時間軸,可以清晰地看到,直到 CSS 下載完成后才執(zhí)行。

「譯文」:CSS 與網(wǎng)絡(luò)性能,你不看必定吃虧!

注意,你應(yīng)該根據(jù)頁面的實(shí)際情況測試這種代碼組織方式,取決于 CSS 與 JavaScript 文件大小與 JavaScript 文件執(zhí)行所需的時間,可能會出現(xiàn)不同的結(jié)果。記得多測試!(譯者注:根據(jù)實(shí)踐經(jīng)驗(yàn),<head> 中的代碼組織基本可以按照這種方式,即 JS 在 CSS 之前,因?yàn)?<head> 中的 JS 代碼基本不依賴 CSS,唯一的反例是 JS 代碼體積非常大或執(zhí)行時間很長。)

將 <link rel="stylesheet" /> 放在 <body> 中。


最后一條優(yōu)化策略比較新穎,它對頁面性能有很大幫助,并使頁面達(dá)到逐步渲染的效果,同時易于執(zhí)行。

在 HTTP/1.1 中,我們習(xí)慣于將全部的 css 打成一個文件,如 app.css:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><html>
<head>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<header class="site-header">
<nav class="site-nav">...</nav>
</header>
<main class="content">
<section class="content-primary">
<h1>...</h1>
<div class="date-picker">...</div>
</section>
<aside class="content-secondary">
<div class="ads">...</div>
</aside>
</main>
<footer class="site-footer">
</footer>
</body>
復(fù)制代碼
</pre>

然而,從三方面而言,渲染性能降低了:

  1. 每個頁面只用到 app.css 中的部分樣式: 用戶會下載多余的 CSS。
  2. 難以制定緩存策略: 例如,某個頁面使用的日期選擇器更改了背景顏色,重新生成 app.css 后,舊的 app.css 緩存將失效。
  3. 整個 app.css 在解析構(gòu)建完 CSSOM 之前,頁面渲染被阻塞: 盡管當(dāng)前頁面可能只用到了 17% 的 CSS代碼,但(瀏覽器)仍需等待其他 83% 的代碼下載并解析完后,才能開始渲染。

使用 HTTP/2,可以解決第一與第二點(diǎn):

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><html>
<head>
<link rel="stylesheet" href="core.css" />
<link rel="stylesheet" href="site-header.css" />
<link rel="stylesheet" href="site-nav.css" />
<link rel="stylesheet" href="content.css" />
<link rel="stylesheet" href="content-primary.css" />
<link rel="stylesheet" href="date-picker.css" />
<link rel="stylesheet" href="content-secondary.css" />
<link rel="stylesheet" href="ads.css" />
<link rel="stylesheet" href="site-footer.css" />
</head>
<body>
<header class="site-header">
<nav class="site-nav">...</nav>
</header>
<main class="content">
<section class="content-primary">
<h1>...</h1>
<div class="date-picker">...</div>
</section>
<aside class="content-secondary">
<div class="ads">...</div>
</aside>
</main>
<footer class="site-footer">
</footer>
</body>
復(fù)制代碼
</pre>

根據(jù)頁面的不同組件下載不同的 CSS,能有效地解決冗余問題。這減少了對關(guān)鍵路徑造成阻塞的 CSS 文件總大小。

同時,我們可以制定更有效的緩存策略,(當(dāng)代碼產(chǎn)生變化之后,)只會影響對應(yīng)文件的緩存,其他的文件保持不變。

但仍有解決的問題:下載并解析全部 CSS 文件之前,頁面的渲染仍然是阻塞的。頁面的渲染時間仍然取決于最慢的 CSS 文件下載與解析的時間。假設(shè)由于某種原因,頁腳的 CSS 下載需要很長時間,(即使頁頭的 CSSOM 已經(jīng)構(gòu)建完成,)瀏覽器也只能等待而無法渲染頁頭。

然而,這現(xiàn)象在 Chrome (v69)中得到緩解,F(xiàn)irefox 與 IE/Edge 也已經(jīng)進(jìn)行了相關(guān)的優(yōu)化。<link rel="stylesheet" /> 只會阻塞后續(xù)內(nèi)容,而不是整個頁面的渲染。這意味著我們可以用以下方式組織代碼:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><html>
<head>
<link rel="stylesheet" href="core.css" />
</head>
<body>
<link rel="stylesheet" href="site-header.css" />
<header class="site-header">
<link rel="stylesheet" href="site-nav.css" />
<nav class="site-nav">...</nav>
</header>
<link rel="stylesheet" href="content.css" />
<main class="content">
<link rel="stylesheet" href="content-primary.css" />
<section class="content-primary">
<h1>...</h1>
<link rel="stylesheet" href="date-picker.css" />
<div class="date-picker">...</div>
</section>
<link rel="stylesheet" href="content-secondary.css" />
<aside class="content-secondary">
<link rel="stylesheet" href="ads.css" />
<div class="ads">...</div>
</aside>
</main>
<link rel="stylesheet" href="site-footer.css" />
<footer class="site-footer">
</footer>
</body>
復(fù)制代碼
</pre>

這樣的結(jié)果是我們能逐步渲染頁面,當(dāng)前面的 CSS 可用時,頁面將呈現(xiàn)對應(yīng)的內(nèi)容(,而不需等待全部 CSS 下載并解析完畢)。

I如果瀏覽器不支持這種特性,也不會損害頁面的性能。整個頁面將回退為原來的模式,只有在最慢的 CSS 下載并解析完成后,才能渲染頁面。

有關(guān)這種特性的更多細(xì)節(jié),建議閱讀這篇文章。

總結(jié)


本文內(nèi)容比較 繁雜,成文后超出了本來的預(yù)期,嘗試總結(jié)了 CSS 加載相關(guān)的一系列的最佳實(shí)踐,值得仔細(xì)體會:

  • 懶加載非關(guān)鍵 CSS:
  • 優(yōu)先加載關(guān)鍵 CSS,懶加載其他 CSS;
  • 或根據(jù)媒體類型拆分 CSS 文件。
  • 避免使用 @import:
  • 在 HTML 文檔中應(yīng)該避免;
  • 在 CSS 文件之中更應(yīng)避免;
  • 以及警惕預(yù)加載掃描器的怪異行為。
  • 關(guān)注 CSS 與 JavaScript 的順序:
  • 在 CSS 文件后的 JavaScript 僅在 CSSOM 構(gòu)建完成后才會執(zhí)行;
  • 如果你的 JavaScript 不依賴 CSS;
  • 將它放置于 CSS 之前;
  • 如果 JavaScript 依賴 CSS:
  • 將它放置于 CSS 之后。
  • 僅加載 DOM 依賴的 CSS:
  • 這將提高初次渲染的速度使讓頁面逐步渲染。

注意

本文敘述的內(nèi)容都遵循規(guī)范或根據(jù)瀏覽器的行為推導(dǎo)得出,然而,你應(yīng)該親自進(jìn)行測試。盡管理論上是正確的,但在實(shí)踐中可能會有所不同。記得好好測試!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容