【非原創(chuàng)】對《CSS and Network Performance》一文的總結

原文地址(作者:Harry):CSS and Network Performance
譯文地址(作者:sea_ljf):CSS 與網(wǎng)絡性能

前幾天讀了這篇文章感覺寫的很不錯,所以簡單做些筆記以及摘錄方便后期查閱(本文非原創(chuàng))。



為什么要通過 css 來提高性能?

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

css 是渲染性能的關鍵這是因為:

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

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

方法一:使用關鍵 CSS(最有效的方法)

關鍵css如何實現(xiàn)?

找出首次渲染所需的樣式(通常是首屏相關的樣式),將它們內(nèi)聯(lián)到 <head> 標簽中,其他樣式則通過異步的方式進行加載。

雖然這十分有效,但實施起來卻并不容易,比如:高度動態(tài)化的網(wǎng)站通常難以提取首屏相關的樣式、提取的過程需要自動化、需要對首屏不同元素顯示或隱藏的狀態(tài)作出假設、某些邊界情況難以處理以及相關工具仍未成熟等問題。如果你的項目相當龐大或是有歷史包袱,這將變得更為復雜。如果項目難以執(zhí)行關鍵 CSS 策略,可以嘗試根據(jù)媒體查詢拆分 CSS 文件,這也是一種可靠的策略。

方法二:根據(jù)媒體查詢拆分 CSS 文件

  1. 以非常高的優(yōu)先級下載符合當前上下文(設備、屏幕尺寸、分辨率、方向等)的 CSS 文件,阻塞關鍵路徑;
  2. 以非常低的優(yōu)先級下載不符合當前上下文的 CSS 文件,不會阻塞關鍵路徑。
  3. 瀏覽器基本上能將未命中媒體查詢的 CSS 文件延遲下載。
  4. 瀏覽器仍然會下載全部的 CSS 文件,但只有符合當前上下文的 CSS 文件會阻塞渲染。
<link rel="stylesheet" href="./all.css" media="all" />
<link rel="stylesheet" href="./a.css" media="(max-width: 300px)"/>
<link rel="stylesheet" href="./b.css" media="(max-width: 500px)"/>
<link rel="stylesheet" href="./c.css" media="(max-width: 800px)"/>
<link rel="stylesheet" href="./d.css"/>

我們把css文件拆封成上面 all、a、b、c、d 五個文件通過改變?yōu)g覽器寬度發(fā)現(xiàn)請求規(guī)律是下面這樣的:

all.css > 被媒體查新命中的css文件 > d.css > 未被媒體查詢命中的css文件

總結:all.css 之所以被首先加載是因為 all.css 的media="all" 所以 all.css 始終是被命中的,而且 all.css 也是最先被引入的,證明了瀏覽器會優(yōu)先加載被媒體查詢命中的css文件,多個文件同時被命中時請求順序以引入的先后順序決定。

為什么上文反復提到了阻塞路徑,我們用 link 引入了多個css有被媒體查詢擊中的也有未被擊中的,那么只有被擊中的才會阻塞路徑這這樣有什么好處呢,我們想如果路徑不被阻塞那么在css加載過程 html,js 也是同步加載的,這時候會有頁面裸體的情況出現(xiàn),而且如果這個時候DOM沒有得到css的渲染那么我們用js去獲取DOM的屬性比如寬高是不是會有問題,所以要等css加載完瀏覽器才去渲染DOM,link 可以異步加載(多個link同時加載)但是這個時候DOM并不會渲染,上文的阻塞路徑就是說只要被媒體查詢擊中的css加載完成DOM就開始渲染了,不會等未被擊中的css加載。

方法三:避免在 CSS 文件中使用 @import

如果了解 @import 的原理,那應該清楚它的性能并不高,使用它會阻塞渲染更長時間。這是因為我們在關鍵路徑上創(chuàng)造了更多隊列式的網(wǎng)絡請求,如果使用了 @import 那么請求過程是這樣的:

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

也就是說使用 @import 加載的css是不會并行加載的,他會阻塞頁面,這和我們開始的初衷相違背。

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

方法四:不要將動態(tài)插入 JavaScript 的代碼放在 link 之后

所有瀏覽器都存在一個鮮為人知,但符合邏輯的現(xiàn)象:在瀏覽器下載完該 CSS 文件之前,不會執(zhí)行下面的 JS

<link rel="stylesheet" href="a.css" />
<script> console.log('hello word!') </script>

這是合理的。當 CSS 文件尚未下載完成時,HTML 文檔中任何同步的 JavaScript 代碼,均不會執(zhí)行??紤]以下場景: script 中的代碼會訪問當前的頁面樣式,為確保結果正確,需要等待 script 標簽前所有 CSS 文件下載并解析完畢后再獲取,否則無法保證正確性。因此,在 css 渲染完成之前 css 后面的 js 不會執(zhí)行,這也是為啥要把 link 方在 script 之前的原因。

看下面的代碼示例:

<div id="box">asas</div>
<script> console.log(box.offsetWidth) // 寬度是屏幕寬度 </script>
<style>#box {width: 100px;height: 100px;background: pink;}</style>

<div id="box">asas</div>
<style>#box {width: 100px;height: 100px;background: pink;}</style>
<script> console.log(box.offsetWidth) // 寬度是100px </script>

總結:

  • 如果 link 在 script 之前那么 script 要等 link 加載完才去執(zhí)行以保證樣式的正確獲取。
  • 如果 script 在 link 之前那么 script 不會等待 link 的渲染這樣樣式獲取就會有問題。
  • 綜上如果 script 是依賴 link 的那么必須要把 script 放在 link 下面。

看下面的例子:

<link rel="stylesheet" href="app.css"/>
<script>
  var script = document.createElement('script')
  script.src = "a.js"
  document.getElementsByTagName('head')[0].appendChild(script)
</script>

如果我們將一個 link 放在 script 之前,script 中動態(tài)創(chuàng)建新 script 的代碼只會在 CSS 文件下載完之后才會執(zhí)行,這意味著 CSS 推遲了資源的下載與執(zhí)行,完全失去了并行下載的優(yōu)勢。

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

總結:
在 head 中將無需查詢 CSSOM 的 JavaScript 代碼放在 CSS 文件之前,需要查詢的放在 CSS 文件之后,這樣即便有動態(tài)生成的 script 資源也可以得到提前的簡析和加載。對于第三方的 script 資源不可一概而論如非必要,放在頁面末尾或空閑時下載及執(zhí)行也未嘗不可。(盡管執(zhí)行 JavaScript 代碼時會停止解析 DOM, 但預加載掃描器會提前下載之后的 CSS)

注:如果你一部分 JavaScript 需要依賴 CSS 而另一部分卻不用,最佳的實踐是將 JavaScript 分為兩部分,分別置于 CSS 的兩側。根據(jù)這種組織方式,我們的頁面會按最佳的方式下載與執(zhí)行相關代碼。

注意:head 中的代碼組織基本可以按照這種方式,即 JS 在 CSS 之前,因為 head 中的 JS 代碼基本不依賴 CSS,唯一的反例是 JS 代碼體積非常大或執(zhí)行時間很長。

方法五:合理構建 css 文件

我們習慣于將全部的 css 打成一個文件如 style.css,然而從三方面而言,渲染性能降低了:

  1. 每個頁面只用到 style.css 中的部分樣式用戶會下載多余的 CSS。
  2. 難以制定緩存策略,例如服務端重新生成 style.css 后,舊的 style.css 緩存將失效。
  3. 整個 style.css 在解析構建完 CSSOM 之前,頁面渲染被阻塞,盡管當前頁面可能只用到了 17% 的 CSS代碼,但(瀏覽器)仍需等待其他 83% 的代碼下載并解析完后,才能開始渲染。

所以合理拆分 css,制定合理的 css 加載策略對性能是有一定好處的。


全文總結:

  • 懶加載非關鍵 CSS:優(yōu)先加載關鍵 CSS,懶加載其他 CSS;或根據(jù)媒體類型拆分 CSS 文件。
  • 避免使用 @import:在 HTML 文檔中應該避免;在 CSS 文件之中更應避免;以及警惕預加載掃描器的怪異行為。
  • 關注 CSS 與 JavaScript 的順序:
    • 在 CSS 文件后的 JavaScript 僅在 CSSOM 構建完成后才會執(zhí)行;
    • 如果你的 JavaScript 不依賴 CSS;將它放置于 CSS 之前;
    • 如果 JavaScript 依賴 CSS:將它放置于 CSS 之后。
  • 僅加載 DOM 依賴的 CSS:這將提高初次渲染的速度使讓頁面逐步渲染。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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