CSS和網絡性能

CSS對於呈現頁面相當重要 - 在找到,下載和解析全部CSS以前,瀏覽器不會開始呈現 - 所以咱們必須儘量快地將其加載到用戶的設備上。 關鍵路徑上的任何延遲都會影響咱們的「開始渲染」並讓用戶看到空白屏幕。css

什麼是大問題?

從廣義上講,這就是CSS對性能相當重要的緣由:html

  • 瀏覽器在構建渲染樹以前沒法渲染頁面;
  • 渲染樹是DOM和CSSOM的組合結果;
  • DOM是HTML加上須要對其進行操做的任何阻塞JavaScript;
  • CSSOM是針對DOM應用的全部CSS規則;
  • 使用async和defer屬性很容易使JavaScript無阻塞;
  • CSS不容易異步;
  • 因此要記住的一個好的經驗法則是,您的頁面會在你最慢的樣式表加載完成以後才展現。

考慮到這一點,咱們須要儘快構建DOM和CSSOM。 在大多數狀況下,構建DOM相對較快:您的第一個HTML響應是DOM。 可是,因爲CSS幾乎老是HTML的子資源,所以構建CSSOM一般須要更長的時間。程序員

在這篇文章中,我想看看CSS如何證實是網絡上的一個重大瓶頸(自己和其餘資源)以及咱們如何緩解它,從而縮短關鍵路徑並縮短開始渲染的時間。瀏覽器

使用關鍵CSS

若是你有能力,減小Start Render時間的最有效方法之一就是使用Critical CSS模式:識別Start Render所需的全部樣式(一般是首屏所需的樣式), 將它們內聯到文檔的<head>中的<style>標記中,並從這裏異步加載剩餘的樣式表。緩存

雖然這種策略是有效的,但並不簡單:高度動態的網站很難從中提取樣式,流程須要自動化,咱們必須對摺疊率甚至是什麼作出假設,很難捕獲邊緣狀況和工具 仍處於相對初期階段。 若是您正在使用大型或遺留代碼庫,事情會變得更加困難......安全

拆分媒體類型

若是實現關鍵CSS很是棘手 - 它可能只是一種選擇,咱們將主要的CSS文件拆分爲其各自的媒體查詢。 這樣作的實際結果是瀏覽器會......微信

  • 以很是高的優先級下載當前上下文所需的任何CSS(中等,屏幕大小,分辨率,方向等),阻止關鍵路徑;
  • 以很是低的優先級下載當前上下文不須要的任何CSS,徹底脫離關鍵路徑。

基本上,瀏覽器有效地延遲了不須要渲染當前視圖的任何CSS。網絡

<link rel="stylesheet" href="all.css" />

若是咱們將全部CSS捆綁到一個文件中,那麼它會這樣子加載:app

clipboard.png

若是咱們能夠將單個全渲染阻塞文件拆分爲各自的媒體查詢:dom

<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" />

而後咱們看到網絡以不一樣方式處理文件:

clipboard.png

瀏覽器仍將下載全部CSS文件,但它只會阻止渲染完成當前上下文所需的文件。

避免在CSS文件中使用@import

咱們能夠作的下一件事就是幫助Start Render更加簡單。 避免在CSS文件中使用@import。

@import,根據它的工做原理,很慢。 對於Start Render性能來講真的很是糟糕。 這是由於咱們正在關鍵路徑上積極建立更多循環路徑:

  1. 下載HTML;
  2. HTML請求CSS;(這是咱們但願可以構建渲染樹的地方,可是;)
  3. CSS請求更多CSS;
  4. 構建渲染樹。

如下HTML:

<link rel="stylesheet" href="all.css" />

包含在all.css中@import

@import url(imported.css);

咱們最終獲得這樣的瀑布圖:

clipboard.png

經過簡單地將其展平爲兩個<link rel =「stylesheet」/>和去掉@imports:

<link rel="stylesheet" href="all.css" />
<link rel="stylesheet" href="imported.css" />

咱們獲得一個更健康的瀑布圖:

clipboard.png

請注意HTML中的@import

要徹底理解本節,咱們首先須要瞭解瀏覽器的預裝載掃描程序:全部主流瀏覽器都實現了一般稱爲預裝載掃描程序的輔助惰性解析器。 瀏覽器的主要解析器負責構建DOM,CSSOM,運行JavaScript等,而且隨着文檔的不一樣部分阻止它而不斷中止和啓動。 Preload Scanner能夠安全地跳過主解析器並掃描HTML的其他部分,以發現對其餘子資源(例如CSS文件,JS,圖像)的引用。 一旦發現它們,Preload Scanner就會開始下載它們,以便主要解析器接收它們並在之後執行/應用它們。 Preload Scanner的推出使網頁性能提升了大約19%,全部這些都不須要開發人員參與。 這對用戶來講是個好消息!

咱們做爲開發人員須要警戒的一件事是無心中隱藏了Preload Scanner中可能發生的事情。 稍後會詳細介紹。

本節介紹WebKit和Blink的Preload Scanner中的錯誤,以及Firefox和IE / Edge的Preload Scanner中的低效率。

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

在Firefox和IE / Edge中,Preload Scanner彷佛沒有使用<script src =「」>或<link rel =「stylesheet」/>以後定義的任何@import。

這意味着這個HTML:

<script src="app.js"></script>

<style>
  @import url(app.css);
</style>

將產生這個瀑布圖:

clipboard.png

因爲無效預裝載掃描程序致使Firefox失去並行化(N.B.在IE / Edge中出現相同的瀑布。)

這個問題的直接解決方案是交換<script>或<link rel =「stylesheet」/>和<style>塊。 可是,當咱們更改依賴順序時,這可能會破壞事物(想一想他們之間的關聯)。

這個問題的首選解決方案是徹底避免使用@import並使用第二個<link rel =「stylesheet」/>:

<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="app.css" />

瀑布圖以下:

clipboard.png

兩個<link rel =「stylesheet」/> s讓咱們回到並行化。 (N.B. IE / Edge中出現相同的瀑布。)

Blink和WebKit:用HTML格式引用@import URL

僅當您的@import URL缺乏引號(「)時,WebKit和Blink的行爲與Firefox和IE / Edge徹底相同。這意味着WebKit和Blink中的Preload Scanner存在錯誤。

簡單地將@import包裝在引號中將解決問題,您無需從新排序任何內容。 不過,和之前同樣,個人建議是徹底避免使用@import,而是選擇第二個<link rel =「stylesheet」/>。

以前

<link rel="stylesheet" href="style.css" />

<style>
  @import url(app.css);
</style>

瀑布圖:

clipboard.png

咱們的@ import網址中缺乏引號會破壞Chrome的預裝掃描程序(N.B.在Opera和Safari中會出現相同的瀑布。)

修改以後:

<link rel="stylesheet" href="style.css" />

<style>
  @import url("app.css");
</style>

clipboard.png

在咱們的@ import網址中添加引號可修復Chrome的Preload Scanner(N.B.在Opera和Safari中也會出現相同的瀑布。)

這絕對是WebKit / Blink中的一個錯誤 - 缺乏引號不該該隱藏Preload Scanner中的@imported樣式表。

不要在Async 腳本以前放置<link rel =「stylesheet」/>

上一節討論瞭如何經過其餘資源減慢CSS,本節將討論CSS如何無心中延遲下載資源的下載,主要是使用異步加載代碼段插入的JavaScript,以下所示:

<script>
  var script = document.createElement('script');
  script.src = "analytics.js";
  document.getElementsByTagName('head')[0].appendChild(script);
</script>

在全部瀏覽器中都存在一種有意和預期的迷人行爲,但我從未遇到過一個瞭解它的開發人員。 當您考慮它能夠帶來的巨大性能影響時,這是很是使人驚訝的:

若是有任何當前CSS在加載,瀏覽器將不會執行<script>。

<link rel="stylesheet" href="slow-loading-stylesheet.css" />
<script>
  console.log("I will not run until slow-loading-stylesheet.css is downloaded.");
</script>

這是設計的。 這是故意的。 當前正在下載任何CSS時,HTML中的任何同步<script>都不會執行。 這是一個簡單的防護策略來解決<script>可能會詢問頁面樣式的邊緣狀況:若是腳本在CSS到達並被解析以前詢問頁面的顏色,那麼JavaScript給咱們的答案 多是不正確或陳舊的。 爲了緩解這種狀況,瀏覽器在構造CSSOM以前不會執行<script>。

這樣作的結果是,CSS下載時間的任何延遲都會對你的異步片斷產生連鎖反應。 用一個例子能夠很好地說明這一點。

若是咱們在異步片斷前放置<link rel =「stylesheet」/>,則在下載和解析該CSS文件以前它不會運行。

<link rel="stylesheet" href="app.css" />

<script>
  var script = document.createElement('script');
  script.src = "analytics.js";
  document.getElementsByTagName('head')[0].appendChild(script);
</script>

根據這個順序,咱們能夠清楚地看到JavaScript文件甚至在構建CSSOM以前甚至沒有開始下載。 咱們徹底失去了任何並行化:

clipboard.png

在異步代碼段以前使用樣式表能夠撤消咱們並行化的機會。
有趣的是,Preload Scanner但願提早得到對analytics.js的引用,可是咱們無心中隱藏了它:「analytics.js」是一個字符串,而且在<<以前不會成爲可標記的src屬性 script>元素存在於DOM中。 這是我早些時候說的,當我稍後再說這個時。

第三方供應商提供這樣的異步代碼片斷以更安全地加載腳本是很常見的。 開發人員對這些第三方持懷疑態度,並在頁面後面放置異步片斷也是很常見的。 雖然這是出於最好的意圖 - 我不想在我本身的資產以前放置第三方<script>! - 一般多是淨損失。 事實上,谷歌分析甚至告訴咱們該作什麼,他們是對的:

將此代碼做爲第一項複製並粘貼到您要跟蹤的每一個網頁的<HEAD>中。

因此個人建議是:

若是您的<script> ... </ script>塊不依賴於CSS,請將它們放在樣式表上方。

如下是咱們轉向此模式時會發生的代碼:

<script>
  var script = document.createElement('script');
  script.src = "analytics.js";
  document.getElementsByTagName('head')[0].appendChild(script);
</script>

<link rel="stylesheet" href="app.css" />

clipboard.png

交換樣式表和異步代碼片斷能夠從新得到並行化。

如今您能夠看到咱們已經徹底從新得到了並行化,而且頁面加載速度提升了近2倍。

在CSS以前放置任何非CSSOM查詢JavaScript; 在CSS以後放置任何CSSOM查詢JavaScript

更進一步,除了異步加載片斷以外,咱們應該如何更普適地加載CSS和JavaScript? 爲了解決這個問題,我提出瞭如下問題並從那裏開始工做:

若是:

  • 在CSSOM構造上阻止CSS後定義的同步JS;
  • 同步JS阻止DOM構造

那麼 - 假設沒有相互依賴 - 哪一個更快/更喜歡?

Script -> style;
style -> script?

答案是:

若是文件不相互依賴,那麼您應該將阻塞腳本置於阻塞樣式之上 - 沒有必要將JavaScript執行延遲到JavaScript實際上不依賴的CSS。

(Preload Scanner確保即便在腳本上阻止了DOM構造,CSS仍然會並行下載。)

若是你的一些JavaScript作了但有些不依賴於CSS,那麼加載同步JavaScript和CSS的絕對最佳順序是將JavaScript分紅兩部分並將其加載到CSS的任何一側:

<!-- This JavaScript executes as soon as it has arrived. -->
<script src="i-need-to-block-dom-but-DONT-need-to-query-cssom.js"></script>

<link rel="stylesheet" href="app.css" />

<!-- This JavaScript executes as soon as the CSSOM is built. -->
<script src="i-need-to-block-dom-but-DO-need-to-query-cssom.js"></script>

使用這種加載模式,咱們能夠按最佳順序進行下載和執行。 我爲下面的截圖中的微小細節道歉,但但願你能看到表明JavaScript執行的小粉紅色標記。
entry(1)是計劃在其餘文件到達和/或執行時執行某些JavaScript的HTML;
entry(2)執行它到達的那一刻;
entry(3)是CSS,因此不執行任何JavaScript;
在CSS完成以前,entry(4)實際上不會執行。

clipboard.png

注: 您必須根據本身的特定用例測試此模式:根據您以前的CSS JavaScript文件與CSS自己之間的文件大小和執行成本是否存在巨大差別,可能會有不一樣的結果。 測試,測試,測試。

將<link rel =「stylesheet」/>放在<body>中

這個最終策略是一個相對較新的策略,對感知性能和漸進式渲染有很大好處。 它也很是友好。

在HTTP / 1.1中,咱們將全部樣式鏈接到一個主要包中是很典型的。 咱們稱之爲app.css:

<!DOCTYPE html>
<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>

這帶來三個關鍵的低效率:

  • 任何給定的頁面只會使用app.css中的一小部分樣式:咱們幾乎確定會下載比咱們須要的更多的CSS。
  • 咱們受限於一種效率低下的緩存策略:例如,對僅在一個頁面上使用的日期選擇器上當前所選日期的背景顏色進行更改將須要咱們緩存整個app.css。
  • 整個app.css阻止渲染:若是當前頁面只須要17%的app.css並不重要,咱們仍然須要等待其餘83%才能開始渲染。

使用HTTP / 2,咱們能夠開始解決點(1)和(2):

<!DOCTYPE html>
<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>

如今咱們正在解決冗餘問題,由於咱們可以加載更適合頁面的CSS,而不是不加選擇地下載全部內容。 這減小了關鍵路徑上阻塞CSS的大小。

咱們還能夠採用更有意思的緩存策略,只緩存破壞須要它的文件,並保持其他部分不受影響。

咱們尚未解決的問題是它仍然阻止渲染 - 咱們仍然只有最慢的樣式表。 這意味着若是不管出於何種緣由,site-footer.css須要很長時間才能下載,瀏覽器沒法開始渲染.site-header。

可是,因爲Chrome最近發生了變化(我相信版本69),以及Firefox和IE / Edge中已經存在的行爲,<link rel =「stylesheet」/> 只會阻止後續內容的呈現,而不是 整頁。 這意味着咱們如今可以像這樣構建咱們的頁面:

<!DOCTYPE html>
<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>

這樣作的實際結果是,咱們如今可以逐步呈現咱們的頁面,在頁面可用時有效地將頁面輸送樣式添加到頁面中。

在目前不支持這種新行爲的瀏覽器中,咱們不會遇到性能降低:咱們會回到原來的行爲,咱們只有最慢的CSS文件加載完成纔會展現頁面。

總結

本文中有不少要消化的內容。 它最終超越了我最初打算寫的帖子。 嘗試總結加載CSS的最佳網絡性能實踐:

  • Lazyload Start Start Render不須要的任何CSS:

    拆分關鍵CSS;
       或將您的CSS拆分爲媒體查詢。
  • 避免@import:

    在你的HTML中;
       特別是在CSS中;
       並提防Preload Scanner的奇怪之處。
  • 警戒同步CSS和JavaScript命令:

    在CSSOM完成以前,CSS以後定義的JavaScript將沒法運行
       因此若是你的JavaScript不依賴於你的CSS,在CSS以前加載它;
       若是它取決於你的CSS,在CSS以後加載它。
  • 在DOM須要時加載CSS,這將取消阻止「開始渲染」並容許漸進式渲染

我上面概述的全部內容都遵循規範或已知/預期的行爲,可是,一如既往,本身測試一切。 雖然這在理論上都是正確的,但在實踐中事情老是有所不一樣。 套用中國的一句老話,實踐出真知啊

建立了一個程序員交流微信羣,你們進羣交流IT技術

圖片描述

若是已過時,能夠添加博主微信號15706211347,拉你進羣

相關文章
相關標籤/搜索