原文連接:CSS and Network Performancecss
挺長的一篇文章,比較全面地介紹了 CSS 加載的相關知識,因爲譯者水平有限,有能力的同窗建議直接看原文,同時也但願譯文對你有所幫助,謝謝~如下是正文:html
承蒙擡愛,我被稱爲 CSS 魔術師已經十多年了,但最近在博客上,CSS 相關的文章卻很少。那就結合 CSS 與性能這兩大主題,爲你們帶來一篇文章吧。web
CSS 是頁面渲染的關鍵因素之一,(當頁面存在外鏈 CSS 時,)瀏覽器會等待所有的 CSS 下載及解析完成後再渲染頁面。關鍵路徑上的任何延遲都會影響首屏時間,於是咱們須要儘快地將 CSS 傳輸到用戶的設備,不然,(在頁面渲染以前,)用戶只能看到一個空白的屏幕。瀏覽器
廣義而言,CSS 是(渲染)性能的關鍵,這是因爲:緩存
基於上述考慮,咱們須要儘快構建 DOM 與 CSSOM。通常狀況下,DOM 的構建是相對較快,(當請求某個頁面時,)服務器響應的首個請求是 HTML 文檔。但通常 CSS 是做爲 HTML 的子資源而存在,所以 CSSOM 的構建一般須要更長的時間。安全
在這篇文章中,會講述 CSS 爲什麼是網絡瓶頸(不管是對於它本身或是其餘資源),該如何突破它,從而縮短關鍵路徑以減小首次渲染前的等待時間。服務器
若是條件容許,縮短渲染前等待時間最有效的方式就是使用 Critical CSS (關鍵 CSS)模式:找出首次渲染所需的樣式(一般是首屏相關的樣式),將它們內聯到 <head>
標籤中,其餘樣式則經過異步的方式進行加載。網絡
雖然這十分有效,但實施起來卻並不容易,好比:高度動態化的網站(譯者注:如 SPA)一般難以提取首屏相關的樣式、提取的過程須要自動化、須要對首屏不一樣元素顯示或隱藏的狀態做出假設、某些邊界狀況難以處理以及相關工具仍未成熟等問題。若是你的項目至關龐大或是有歷史包袱,這將變得更爲複雜。app
若是在項目組難以執行關鍵 CSS 策略,能夠嘗試根據媒體查詢拆分 CSS 文件,這也是一種可靠的策略。執行此策略後,瀏覽器表現以下:dom
瀏覽器基本上能將未命中媒體查詢的 CSS 文件延遲下載。
<link rel="stylesheet" href="all.css" />
複製代碼
若是咱們把所有的 CSS 代碼都放在一個文件中,請求的表現以下:
咱們能夠觀察到,這個單獨的 CSS 文件會以 最高 的優先級下載。
根據媒體查詢拆分紅若干個 CSS 文件後:
<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" />
複製代碼
瀏覽器會以不一樣的優先級下載 CSS 文件:
瀏覽器仍然會下載所有的 CSS 文件,但只有符合當前上下文的 CSS 文件會阻塞渲染。
@import
爲縮短渲染等待時間而努力的下一項任務很是簡單:避免在 CSS 文件中使用 @import
若是瞭解 @import
的原理,那應該清楚它的性能並不高,使用它會阻塞渲染更長時間。這是由於咱們在關鍵路徑上創造了更多(隊列式)的網絡請求:
如下是相關的案例:
<link rel="stylesheet" href="all.css" />
複製代碼
all.css 的內容:
@import url(imported.css);
複製代碼
最終,瀏覽器的請求瀑布圖呈現爲:
關鍵路徑上的 CSS 文件並無並行下載。
經過將 @imports
請求的文件改成 <link rel="stylesheet" />
:
<link rel="stylesheet" href="all.css" />
<link rel="stylesheet" href="imported.css" />
複製代碼
能夠提升網絡性能:
關鍵路徑上的 CSS 文件是並行下載的。
注意,有一個特殊的狀況值得討論。若是你沒有包含 @import
的 CSS 文件的修改權限,爲了讓瀏覽器並行下載 CSS 文件,能夠往 HTML 中補充相應的 <link rel="stylesheet" src="@import的地址" />
。瀏覽器會並行下載相應的 CSS 文件且不會重複下載 @import
引用的文件。
@import
本節的內容比較奇怪。各大瀏覽器的相關實現上彷佛都有問題,我之前提交了相關的bugs(譯者注:簡單說,當頁面中存在:<style>@import url(xxx.url);</style>
,瀏覽器不會並行下載,但加上引號後:<style>@import url("xxx.url");</style>
,瀏覽器會並行下載)。
爲了透徹地理解本節的內容,首先咱們須要瞭解瀏覽器的預加載掃描器:各大瀏覽器都實現了一個名爲預加載掃描器的輔助解析器。瀏覽器的核心解析器主要用於構建 DOM、CSSOM、運行 JavaScript 等。HTML 文檔中某些標籤與狀態會阻塞核心解析器,於是核心解析器的運行是斷斷續續的。而預加載掃描器能夠跳到核心解析器還沒有解析的部分,用以發現其餘待引用的子資源(如 CSS、JS 文件、圖片等)。一旦發現此類子資源,預加載掃描器會開始下載它們,以便核心解析器在解析到對應內容時就能使用它們(,而不是直到那一刻纔開始下載該資源)。預加載掃描器的出現,使網頁的加載性能提升了19%,這是一項了不得的成就,能夠極大地優化用戶體驗。
做爲開發者,須要警戒預加載掃描器背後隱藏的問題,這在後文會進行闡述。
在 HTML 中使用 @import
,在以 WebKit 與 Blink 爲內核的瀏覽器中,可能會觸發它們預加載掃描器的 bug,在 Firefox 與 IE/Edge 中,則表現低效。
@import
放在 JS 和 CSS 以前在 Firefox 與 IE/Edge 中,預加載掃描器不會並行下載 <script src="">
和 <link rel="stylesheet" />
後 @imports
引用的資源。
這意味着以下的 HTML:
<script src="app.js"></script>
<style>
@import url(app.css);
</style>
複製代碼
會出現這樣的請求瀑布圖:
因爲預加載掃描器失效,致使資源在 Firefox 中沒法並行下載(IE/Edge 中有着一樣的問題)。
經過上圖,能夠清晰地觀察到:直到 JavaScript 文件下載完成以後,@import
引用的 CSS 文件纔開始下載。
不單 <script>
標籤會觸發此問題,<link>
標籤也會:
<link rel="stylesheet" href="style.css" />
<style>
@import url(app.css);
</style>
複製代碼
與 <script>
標籤同樣,子資源沒法並行下載。
此問題最簡單的解決方案是調換 <script>
或 <link rel="stylesheet" />
標籤與(包含 @import
的)<style>
標籤的位置。然而,當咱們改變順序時,可能會對頁面形成影響。
最佳解決方案是徹底不使用 @import
,再往 HTML 文檔中加入另外一個 <link rel="stylesheet" />
取而代之:
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="app.css" />
複製代碼
修改後,瀏覽器表現更好:
瀏覽器並行下載資源,IE/Edge 表現相同。
@import
時,要用引號包裹 url。對於以 Blink 或 WebKit 爲內核的瀏覽器而言,當 @import
引用的 url 未被引號包裹時,表現與 Firefox 和 IE/Edge 一致(沒法並行下載)。這意味着上述兩個內核的預加載掃描器存在 bug。
所以,無需調整代碼的順序,只須要添加引號便可解決問題。但我仍是建議使用另外一個 <link rel="stylesheet" />
取代 @import
。
未添加引號時的代碼:
<link rel="stylesheet" href="style.css" />
<style>
@import url(app.css);
</style>
複製代碼
瀑布圖:
能夠看到,缺失引號會破壞 Chrome 的預加載(Opera 與 Safari 表現也是如此。)
添加引號後的代碼:
<link rel="stylesheet" href="style.css" />
<style>
@import url("app.css");
</style>
複製代碼
添加引號後,Chrome、Opera 和 Safari 的預加載掃描器表現恢復正常,
這絕對是 WebKit 與 Blink 內核的一個 bug,是否添加引號不該成爲影響預加載掃描器的因素。
感謝 Yoav 幫我追蹤這個問題。
如今這個 bug 現已在 Chromium 的待修復列表中。
<link rel="stylesheet" />
以後在上一節中,咱們瞭解到某些引用 CSS 文件路徑 的方法,會對其餘資源的下載形成負面影響。在本節中,咱們將探究爲什麼稍有不慎,CSS 將延遲其餘資源的下載。該問題主要出如今動態建立的 <script>
標籤中:
<script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
複製代碼
全部瀏覽器都存在一個不爲人知,但符合邏輯的現象,它會對性能形成很大的影響:
在瀏覽器下載完該 CSS 文件以前,不會執行下面的 JS
<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 文檔中任何同步的 JavaScript 代碼,均不會執行。考慮如下場景: <script>
中的代碼會訪問當前的頁面樣式,爲確保結果正確,須要等待( <script>
標籤前)全部 CSS 文件下載並解析完畢後再獲取,不然沒法保證正確性。所以,在 CSSOM 構建完成以前,<script>
中的代碼不會執行。
根據這現象,CSS 文件的下載時間會對後續 <script>
的執行時間形成影響。下面的例子能較好地說明問題。
若是咱們將一個 <link rel="stylesheet" />
放在 <script>
以前,<script>
中動態建立新 <script>
的代碼只會在 CSS 文件下載完以後纔會執行,這意味着 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 構建完成以後纔開始下載,徹底失去了並行下載的優點:
儘管預加載掃描器但願能預下載 analytics.js
,但對 analytics.js
的引用並不是一開始就存在於 HTML 的文檔之中,它是由 <link>
後面 <script>
的代碼動態建立的,在建立以前,它只是一些字符串,而不是預加載掃描器可識別的資源,無形中它被隱藏起來了。
爲了更安全地加載腳本,第三方服務商常常提供這樣的代碼片斷。然而,開發者一般不信任第三方的代碼,於是會把該片斷放在頁面的最後,但這可能會致使不良的後果。事實上,Google Analytics (在文檔中)對此的建議是:
將代碼複製後,做爲第一項粘貼到待追蹤頁面的 中。
綜上,個人建議是:
若是 <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" />
複製代碼
交換位置以後,子資源能夠並行下載,頁面的總體性能提升了兩倍以上。(譯者注:本節的內容只贊成一半,<head>
中的代碼,確實是建議先放 <script>
,再放 <link>
,後文也會有相關的內容,但第三方代碼放在 <head>
中的第一項,取決於相關代碼的用途。如非必要,放在頁面末尾或空閒時下載及執行也何嘗不可)
這條建議遠比你想象中的有用。
上文討論了插入新 <script>
的代碼應放在 <link>
以前,那是否能推廣到其餘的 CSS 與 JavaScript 呢?爲了弄明白這個問題,先提出如下假設:
假設:
那若是 JS 並不依賴 CSSOM,如下那種狀況會更快?
答案是:
若是 JS 文件沒有依賴 CSS,你應該將 JS 代碼放在樣式表以前。 既然沒有依賴,那就沒有任何理由阻塞 JavaScript 代碼的執行。
(儘管執行 JavaScript 代碼時會中止解析 DOM, 但預加載掃描器會提早下載以後的 CSS)
若是你一部分 JavaScript 須要依賴 CSS 而另外一部分卻不用,最佳的實踐是將 JavaScript 分爲兩部分,分別置於 CSS 的兩側:
<!-- 這部分 JavaScript 代碼下載完後會當即執行 -->
<script src="i-need-to-block-dom-but-DONT-need-to-query-cssom.js"></script>
<link rel="stylesheet" href="app.css" />
<!-- 這部分 JavaScript 代碼在 CSSOM 構建完成後纔會執行 -->
<script src="i-need-to-block-dom-but-DO-need-to-query-cssom.js"></script>
複製代碼
根據這種組織方式,咱們的頁面會按最佳的方式下載與執行相關代碼。下面的截圖中,粉色表明 JS 的執行,但它們都比較「纖細」了,但願你能看得清楚。(第一欄的(下同))第一行是整個頁面的時間軸,留意該行粉色的部分,表明 JS 正在執行。第二行是首個 JS 文件的時間軸,能夠看到下載完後並當即執行。第三行是 CSS 的時間軸,於是沒有任何 JS 執行。最後一行是第二個 JS 文件的時間軸,能夠清晰地看到,直到 CSS 下載完成後才執行。
注意,你應該根據頁面的實際狀況測試這種代碼組織方式,取決於 CSS 與 JavaScript 文件大小與 JavaScript 文件執行所需的時間,可能會出現不一樣的結果。記得多測試!(譯者注:根據實踐經驗,<head>
中的代碼組織基本能夠按照這種方式,即 JS 在 CSS 以前,由於 <head>
中的 JS 代碼基本不依賴 CSS,惟一的反例是 JS 代碼體積很是大或執行時間很長。)
<link rel="stylesheet" />
放在 <body>
中。最後一條優化策略比較新穎,它對頁面性能有很大幫助,並使頁面達到逐步渲染的效果,同時易於執行。
在 HTTP/1.1 中,咱們習慣於將所有的 css 打成一個文件,如 app.css:
<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>
複製代碼
然而,從三方面而言,渲染性能下降了:
使用 HTTP/2,能夠解決第一與第二點:
<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 文件總大小。
同時,咱們能夠制定更有效的緩存策略,(當代碼產生變化以後,)只會影響對應文件的緩存,其餘的文件保持不變。
但仍有解決的問題:下載並解析所有 CSS 文件以前,頁面的渲染仍然是阻塞的。頁面的渲染時間仍然取決於最慢的 CSS 文件下載與解析的時間。假設因爲某種緣由,頁腳的 CSS 下載須要很長時間,(即便頁頭的 CSSOM 已經構建完成,)瀏覽器也只能等待而沒法渲染頁頭。
然而,這現象在 Chrome (v69)中獲得緩解,Firefox 與 IE/Edge 也已經進行了相關的優化。<link rel="stylesheet" />
只會阻塞後續內容,而不是整個頁面的渲染。這意味着咱們能夠用如下方式組織代碼:
<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 下載並解析完畢)。
I若是瀏覽器不支持這種特性,也不會損害頁面的性能。整個頁面將回退爲原來的模式,只有在最慢的 CSS 下載並解析完成後,才能渲染頁面。
有關這種特性的更多細節,建議閱讀這篇文章。
本文內容比較 繁雜,成文後超出了原本的預期,嘗試總結了 CSS 加載相關的一系列的最佳實踐,值得仔細體會:
@import
:
本文敘述的內容都遵循規範或根據瀏覽器的行爲推導得出,然而,你應該親自進行測試。儘管理論上是正確的,但在實踐中可能會有所不一樣。記得好好測試!