[譯] CSS 載入機制的未來趨勢

這週閱讀到這篇有意思的文章,於是便動手寫下簡單的翻譯,如果有理解錯誤的地方歡迎指教。

Chrome 正在試圖改變當 <link rel="stylesheet"> 寫在 <body> 的行為,從blink-dev 的文章並不能很清楚的知道其優點。所以這篇文章想要深入的介紹這點。

blink 是 Chrome 和 Opera 渲染引擎,而 blink-dev 是其開發社群

目前的 CSS 載入機制

<head>
  <link rel="stylesheet" href="/all-of-my-styles.css">
</head>
<body>
  …content…
</body>

當 CSS 段落在渲染時,會讓使用者瞪著白白的頁面直到 all-of-my-styles.css 完全下載完畢。

通常我們會把站內所有的 CSS 封裝成較少,可能只有一兩個資源檔,但這同時也意味著使用者需要下載大量的樣式設定(CSS Rules)卻沒有在該頁面使用。因為一個網站包含著各種不同的頁面與元件,而這些東西需要套用不同的樣式規則,如果因此分拆成許多檔案而產生大量的請求 request 這在 HTTP/1 是非常耗效能的。

不過在 SPDYHTTP/2 卻不是這樣,傳輸許多分散的小資源只會增加一點點的開銷,並且這些東西可以個別暫存(cached)。

<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><link> 時,得要知道這些頁面各自需要哪些資源。
另外,瀏覽器在開始輸出之前,仍然得下載所有的 CSS。如果出現一個下載比較慢的 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>

上面程式碼,我們有一些內嵌的樣式(inline style)來讓我們可以快速的渲染初始化的樣式,接著把那些還沒取得樣式的部分隱藏起來,然後開始透過 Javascript 非同步下載剩餘的樣式。這些剩餘的 CSS會覆寫掉在 .main-article 和其他選擇器內的 display: none

這種第一次先快速的初始化渲染,然後持續匯入的方法是許多效能專家所推薦的。

看看範例

原作者實作了wiki-offline並將狀況紀錄如下圖

上面這張圖片是在 3G 的環境下測試。不過這樣的方法還是有些不足的地方:

需要一個輕量的 Javascript 函式庫

不幸的,因為 WebKit 的實作。當 <link rel="stylesheet"> 一被加到頁面時,WebKit 會阻塞渲染(render),直到樣式都被載入,即使這個樣式是透過 Javascript 加入的。

在 Firefox 和 IE/Edge,透過 JS 加入的樣式完全是非同步載入。Chrome 當前穩定版本然仍是遵循 WebKit 的行為,不過在 Canary 已經跟 Firefox/Edge 一樣了。

載入流程被限制在兩個階段(Inline css and A css file)

根據上面的模式,內嵌 CSS(inline CSS) 透過 display: none 隱藏尚未套用樣式的內容,然後非同步得載入 CSS 之後呈現內容。如果您需要增加多個 CSS 檔案那麼結果可能就是內容不按順序出現

檢視範例

如果內容還在異動的過程結果周圍的廣告就先出現這通常會讓使用者覺得不開心就關閉你的網站了。

因此被限制在只有兩個載入階段,你必須要決定哪些是第一次渲染時就要出現,哪些是比較晚的。當然,你希望上方的區塊越快顯示越好,不過所謂"上方的區塊"取決於 viewport 可視區域的大小。最後你可能決定定義一個尺寸範圍套用在所有人身上。

如果你想讓事情更加複雜,當然你可以選擇客製 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"> 在其下載樣式時去阻塞跟在後面的內容,但允許前面的內容先開始渲染。樣式表(CSS)本身的載入機制是平行的,但是套用樣式卻是要照順序的。這讓 <link rel="stylesheet"> 的行為類似為 <script>

假設說 site-header, article, footer 的樣式已經載完了,但剩下的還沒,其行為如下:

  • Header: 已輸出

  • Article: 已輸出

  • Comments: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css)

  • About me: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css)

  • Footer: 未呈現,在該標籤之前地 CSS 還沒載完(./comment.css),即使自己的 CSS 已經載完了

這讓我們可以照順序輸出頁面。您甚至不需要決定哪些是"上面的區塊",只要在元件之前匯入元件需要的 CSS 即可。

不過你還是需要注意當使用內容決定佈局(layout system),例如 table, flexbox 時,在載入期間應避免內容異動。
這不是現在才產生的問題了,只是在逐步顯示這種機制之下更常遇到。如果你看不懂這段在描述什麼看一下下面的影片就知道了。

Youtube 影片連結

意思是說雖然 flexbox 已經很不錯了,但 Grid 還是更推薦的 Layout system。

Chrome 的改變

HTML規範並不涵蓋網頁渲染時是否應該或該如何被 CSS 阻塞,並且也不鼓勵把 <link rel="stylesheet"> 寫在 body 中,不過所有瀏覽器都允許這麼做。

當然他們也都各自使用了自己的方式處理在 body 中的 <link>

  • Chrome & Safari: 一旦發現 <link rel="stylesheet"> 就停止渲染(render),直到發現的樣式被載入完畢。往往導致在 <link> 上面還未渲染的內容被卡住。

  • Firefox: <link rel="stylesheet"><head> 中會阻塞渲染直到所有發現的樣式都被載入完畢。在 body 中的話不會阻塞渲染,除非 head 裡已經有樣式阻塞住
    這可能會導致 FOUC (Flash of unstyled content) ,就是畫面先以預設的樣子呈現後閃一下才套上樣式。

  • IE/Edge: 中斷分析器直到樣式載完,不過允許 <link> 上面的內容渲染(render)。

我們偏好像 IE/Edge 的行為,所以 Chrome 將會跟隨這樣的機制。目前 Chrome/Safari 的行為就是會卡比較久的時間,Firefox 的行為相對較複雜一些,不過有一些小技巧。

Firefox 處理方式

因為 Firefox 不會因為 body 中的 <link> 阻塞渲染。我們需要一點小技巧來避免 FOUC。幸虧有個非常簡單的方式就是透過 <script> 中斷解析器讓他等一等待處理狀態的樣式。

<link rel="stylesheet" href="/article.css"><script> </script>
<main>…</main>

這個標籤不能完全沒有內容,所以我們需要留一個"空白"。

實際執行的結果

查閱範例

Firefox 和 Edge/IE 將會循序的載入輸出,而 Chrome 和 Safari 則是先看到空白的頁面一陣子直到 CSS 全部載完。目前 Chrome/Safari 的行為比起把樣式連結放在 <head> 也沒有差到哪去,所以現在就可以開始使用這個方式,很快的 Chrome 將會往 Edge 的機制修正,這麼一來渲染會更加迅速。

以上就是一個簡單的技巧協助我們加快速度並逐步載入 CSS


andyyou 程序猿生活
記錄關於程序開發生活的筆記與心得
1.7k 声望
616 粉丝
0 条评论
推荐阅读
使用 Rails Webpacker 安裝 Foundation 6
由於 foundation-rails 6.4.1 版本有個 Issue 目前還沒合併。加上 Rails 已經支援了 webpack 2.x。這篇文章純粹紀錄另外一種做法。

andyyu0920阅读 2.2k

还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图
CSS 如何设置自动滚动定位的“安全”间距?
欢迎关注我的公众号:前端侦探介绍两个和滚动定位相关的 CSS 属性:scroll-padding和 scroll-margin在平时开发中,经常会碰到需要快速定位的问题,比如常见的锚点定位 {代码...} 这样,在点击a标签时会自动定位到...

XboxYan30阅读 2.2k评论 2

封面图
CSS transition 小技巧!如何保留 hover 的状态?
欢迎关注我的公众号:前端侦探通常情况下,hover 是无法保存状态的。鼠标移入触发额外样式,一旦移出就还原了 {代码...} 这就意味着,如果需要保留hover的状态,可能就不得不借助JS了,比如下面是某某书院的首页...

XboxYan29阅读 3.5k评论 2

封面图
现代 CSS 之高阶图片渐隐消失术
在过往,我们想要实现一个图片的渐隐消失。最常见的莫过于整体透明度的变化,像是这样: {代码...} {代码...} 但是,CSS 的功能如此强大的今天。我们可以利用 CSS 实现的渐隐效果已经不再是如此的简单。想想看,...

chokcoco25阅读 2.1k

封面图
除了 filter 还有什么置灰网站的方式?
大家都知道,当一些重大事件发生的时候,我们的网站,可能需要置灰,像是这样:当然,通常而言,全站置灰是非常简单的事情,大部分前端同学都知道,仅仅需要使用一行 CSS,就能实现全站置灰的方式。像是这样,我...

chokcoco19阅读 1.6k评论 1

封面图
vue中style scope深度访问新方式(:deep())
1、&gt;&gt;&gt;如果vue的style使用的是css,那么则 {代码...} 但是像scss等预处理器却无法解析&gt;&gt;&gt;,所以我们使用下面的方式.2、/deep/ {代码...} 但是有些开发者反应,在vue-cli3编译时,deep的方式会...

寒水寺一禅11阅读 34.8k评论 9

1.7k 声望
616 粉丝
宣传栏