CSS 加載新方式
Chrome 瀏覽器有意改變 <link rel="stylesheet"> 的加載方式,當其出現在 <body> 中時,這一變化將更加明顯。筆者決定在本文中進行詳細說明這種改變可能帶來影響與好處。
一.目前CSS文件的加載方式
<head>
<link rel="stylesheet" href="/all-of-my-styles.css">
</head>
<body>
…content…
</body>
CSS 會阻礙渲染,因此在 all-of-my-styles.css 全部加載完之前,用戶就只能面對一片空白的屏幕。
通常,我們將某個站點的所有 CSS 樣式合并為一到兩個資源,這意味著用戶會下載一堆當前頁面根本就用不上的規則。這是因為網站可能包含許多不同類型的頁面,每個頁面都有自己的「組件」;而在組件級別傳遞 CSS 的話,會降低 HTTP/1 的性能。
然而,對 SPDY 和 HTTP/2 來說,事實卻并非如此。在這些協議中,許多小資源只需要很小的代價就能完成遞送,并且被獨立緩存。
<head>
<link rel="stylesheet" href="/site-header.css">
<link rel="stylesheet" href="/article.css">
<link rel="stylesheet" href="/comment.css">
<link rel="stylesheet" href="/about-me.css">
<link rel="stylesheet" href="/site-footer.css">
</head>
<body>
…content…
</body>
這樣一來就解決了冗余問題,但也意味著你需要知道輸出 <head> 時頁面將包含的內容,從而防止 streaming。與此同時,瀏覽器還是只能等待所有 CSS 樣式加載完畢,才能開始渲染。如果加載 /site-footer.css 的速度不夠快,就會耽誤所有頁面的渲染。
二.目前最先進的 CSS 加載方法
<head>
<script>
// https://github.com/filamentgroup/loadCSS
!function(e){"use strict"
var n=function(n,t,o){function i(e){return f.body?e():void setTimeout(function(){i(e)})}var d,r,a,l,f=e.document,s=f.createElement("link"),u=o||"all"
return t?d=t:(r=(f.body||f.getElementsByTagName("head")[0]).childNodes,d=r[r.length-1]),a=f.styleSheets,s.rel="stylesheet",s.href=n,s.media="only x",i(function(){d.parentNode.insertBefore(s,t?d:d.nextSibling)}),l=function(e){for(var n=s.href,t=a.length;t--;)if(a[t].href===n)return e()
setTimeout(function(){l(e)})},s.addEventListener&&s.addEventListener("load",function(){this.media=u}),s.onloadcssdefined=l,l(function(){s.media!==u&&(s.media=u)}),s}
"undefined"!=typeof exports?exports.loadCSS=n:e.loadCSS=n}("undefined"!=typeof global?global:this)
</script>
<style>
/* The styles for the site header, plus: */
.main-article,
.comments,
.about-me,
footer {
display: none;
}
</style>
<script>
loadCSS("/the-rest-of-the-styles.css");
</script>
</head>
<body>
</body>
在上面的代碼中,通過一些內聯樣式我們可以加速初始渲染,同時隱藏起還沒有加載完樣式的組件,并通過 JavaScript 異步地完成加載。剩余的 CSS 加載完后會重寫 .main-article 中的 display:none 。
這個方法受到性能專家的推崇,他們認為這是快速完成初始渲染的好方法,并且經過實地測量確實在加載的時候快了不少。
但也存在一些不足之處。。。。。。
「1.它需要一個(小的)JavaScript 庫」
這是由 WebKit 的實現方式造成的。一旦頁面中添加了 <link rel="stylesheet"> ,即使樣式表是由 JavaScript 加載的,WebKit 還是會在加載完成之前阻礙渲染。
在 Firefox 和 IE/Edge 瀏覽器中,通過 JS 加載樣式表是完全異步進行的。穩定版本的 Chrome 瀏覽器是通過 WebKit 的方式加載的,但在 Canary 版本中,仍然是使用 Firefox/Edge 加載方式。
「2.必須經歷兩個加載階段」
在上述模式中,內聯的 CSS 通過 display:none 隱藏了沒有加載完樣式的內容,直到異步加載完剩余的 CSS 樣式。如果你將這些樣式分派到兩個或多個 CSS 文件中,這些文件有可能不按照順序加載,導致加載過程中出現內容錯亂:
內容錯亂,就好比彈出廣告一樣,會導致用戶體驗挫敗,必須全力消滅。
既然有兩個加載階段,你就必須決定渲染的先后順序。你當然會想首先渲染「位置顯要」的內容。但是,所謂的「位置」是根據窗口大小來決定的。因此,問題來了,你得找出一把「萬能」鑰匙。
三.一個更簡單、更好的方法
<head>
</head>
<body>
<!-- HTTP/2 push this resource, or inline it, whichever's faster -->
<link rel="stylesheet" href="/site-header.css">
<header>…</header>
<link rel="stylesheet" href="/article.css">
<main>…</main>
<link rel="stylesheet" href="/comment.css">
<section class="comments">…</section>
<link rel="stylesheet" href="/about-me.css">
<section class="about-me">…</section>
<link rel="stylesheet" href="/site-footer.css">
<footer>…</footer>
</body>
計劃是這樣的:針對每個 <link rel="stylesheet"> ,加載樣式表時我們阻止渲染它的后續內容,但是允許渲染它之前的內容。樣式表是并行加載的,但是按照一定的順序顯示。這使得 <link rel="stylesheet"> 的效用與 <script src="…"></script> 相近。
假設網站 header、正文和 footer 的 CSS 已經加載完畢,但其余內容仍在等待,那么頁面會是這樣的:
- Header:已渲染
- 正文:已渲染
- 評論部分:未渲染,它前面的 CSS 還未被加載( /comment.css )。
- 關于本站:未渲染。它前面的 CSS 還未被加載( /comment.css )。
- Footer:未渲染。盡管它本身的 CSS 已加載完成,但它前面的 CSS 還未被加載( /comment.css )。
這是一個按順序渲染的頁面。你不需要決定哪部分內容在「顯要位置」,只要在頁面組件第一次實例化之前引入該組件的 CSS 即可。它完全兼容 Streaming,因為除非你需要,否則不必要輸出 <link> 。
當使用內容決定布局的布局系統時(例如表格和 flexbox),要注意避免加載時出現內容錯位。這不是什么新問題了,但是分步渲染會使得它出現得更為頻繁。你可以通過 hack flexbox 來解決,但對整體頁面布局來說,使用 CSS grid 工具效果更佳(不過對小一些的組件來說,flexbox 還是很棒的)。
四.Chrome瀏覽器的改變
HTML 規范 并沒有規定 CSS 應當怎樣阻止頁面渲染,它不鼓勵在 body 中使用 <link rel="stylesheet"> ,但是所有的瀏覽器都允許使用。當然了,瀏覽器們在處理 body 中的 link 時都有自己的方法:
-
Chrome和Safari:一旦發現 <link rel="stylesheet"> 就停止渲染,并且在已發現的樣式表全部完成加載之前不會開始渲染。這會導致 <link> 前未被渲染的內容也被阻塞。
-
Firefox:head中的 <link rel="stylesheet"> 會阻塞渲染,直至所有已發現的樣式表加載完畢,body中的 <link rel="stylesheet"> 并不阻塞任何渲染,除非某個 head 中的樣式表已經阻塞了渲染,這會導致無樣式的內容出現閃爍(FOUC)。
-
IE/Edge:阻塞解析器直到樣式表加載完畢,但是允許渲染 <link> 之前的內容。
在 Chrome 團隊,我們喜歡 IE/Edge 的方式,所以打算跟它看齊。這就允許上文描述的漸進式 CSS 渲染方式。我們正在努力把它變成標準,從允許 <body> 中的 <link> 開始。
目前 Chrome/Safari 采用的方式是向下兼容的,帶來的問題是阻塞渲染的時間比實際需要的長。Firefox 的方式稍微復雜一些,但有個解決的方法:
「Firefixing!」
因為 Firefox 并不總是為了 <body> 中的 <link> 阻塞渲染,我們得為這個多花點功夫來避免 FOUC。謝天謝地這很容易,因為 <script> 會阻塞解析,同時也會等掛起的樣式表完成加載:
<link rel="stylesheet" href="/article.css"><script> </script>
<main>…</main>
此處的 <script> 元素必須是非空的,但加個空格足矣。
Firefox 和 Edge/IE 可以實現很美好的漸進式渲染,而 Chrome 和 Safari 在所有 CSS 加載完畢之前只能給你看一張白屏。目前 Chrome/Safari 采用的方式怎么都比將所有的樣式表都放 <head> 里要強,所以你現在就可以開始采用這個方法了。后面幾個月,Chrome 會遷移到 Edge 的模式,這樣用戶就能體驗更快的渲染速度了。
就是這樣!通過更簡單的方法加載你需要的 CSS,強力提升渲染速度。
五.快速定位 CSS 加載問題
那么問題來了,怎么樣才能知道是不是 css 加載 影響了頁面的性能呢?只有定位到問題確實是 css ,老板才會給你時間和人力來優化這方面的問題對不對?
筆者之前做過前端優化的工作,國內外的前端性能優化工具也使用了不少,現階段可以較好實現這個定位頁面慢加載因素的工具有: OneAPM Browser Insight 、 AppDynamics 、 Ruxit ,大家有興趣的話可以去嘗試下。
注:本文原文作者為 Jake Archibald,由 OneAPM 運營人員翻譯整理
原文地址: https://jakearchibald.com/2016/link-in-body/
Browser Insight 是一個基于真實用戶的 Web 前端性能監控平臺,能夠幫大家定位網站性能瓶頸,網站加速效果可視化;支持瀏覽器、微信、App 瀏覽 HTML 和 HTML5 頁面。想閱讀更多技術文章,請訪問OneAPM 官方技術博客。