非同步載入 CSS
本篇文章說明無樣式內容閃爍 FOUC(Flash of unstyled content)、渲染阻塞 (render-blocking) 的問題,以及如何非同步載入 CSS。
無樣式內容閃爍
英文為 FOUC(Flash of unstyled content),這是 HTML 載入後就先顯示出來,之後 CSS 才載入進來,會看到沒有套用樣式的 HTML 畫面。而載入 CSS 可能沒有很久,看起來就像閃了一下。除了這個情況之外,現在有很多網頁也有透過 JavaScript 來載入畫面,也會有類似的問題發生。
例如一個使用 Bootstrap 的頁面放入一個按鈕
1 | <a class="btn btn-primary" href="https://www.google.com/">Google</a> |
如果發生了 FOUC 會有一瞬間看到
當樣式載入後才會看到正確的結果
渲染阻塞
為了避免上面的情況,瀏覽器使用渲染阻塞 (render-blocking) 來處理,會將一些渲染阻塞資源載入後才開始渲染畫面,而 CSS 是渲染阻塞資源之一,瀏覽器的行為會在樣式檔案都載入後開始渲染。上面的例子則會變成下面結果:
進入網頁還沒載入完 CSS 時,可能看網頁在載入中,但畫面並不顯示出來。
當 CSS 載入完之後,直接看到有樣式的結果。
非同步載入 CSS
使用原因
宣染阻塞可能解決了 FOUC 的問題,但似乎又造成新的問題了。假設 CSS 很大或網路很慢的時候,可能的結果就是網頁要等很久才顯示出來,或者甚至一直顯示不出來。另外在使用 PageSpeed 等 SEO 評分的時候,也會把這項列入評分,在 FCP(First Contentful Paint) 和 LCP(Largest Contentful Paint) 的評分影響蠻大。於是使用非同步的方式來載入 CSS,成為一個解決的方式。
方法
在開始非同步之前,先來看一下一般同步載入的方式:
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> |
要改成成非同步常見有幾種方式
- 使用 rel=”preload”
- 使用 media=”print”
- 使用 JavaScript 載入
使用 rel=”preload”
改成
1 | <link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> |
原理是利用 JavaScript onload 事件發生時,才替換成 stylesheet,避免瀏覽器不支援 JavaScript,加上 noscript 部分。注意這個方式如果要設定 media,要再 onload 中設定,不然不會載入:
1 | <link rel="preload" href="print.css" as="style" onload="this.onload=null;this.rel='stylesheet';this.media='print'"> |
使用 media=”print”
改成
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" media="print" onload="this.media='all'"> |
原理是利用 JavaScript onload 事件發生時,才將替換成條件改為可顯示的類型。一開始使用的 print 是列印的時候套用,所以正常進入網頁的時候不會生效。如果是其他 media,除了 print 可以直接用,其他一樣寫在 onload 裏面
1 | <link rel="stylesheet" href="print.css" media="print"> |
使用 JavaScript 載入
可以使用套件 loadCss 或者自己寫,加在頁面載入完成之後:
1 | const link = document.createElement('link'); |
差異
上面三個方法的差異,前兩個差異不大,一般說 preload 的瀏覽器支援程度比差,所以建議使用第二種。而使用 JavaScript 載入,和前兩個差異就比較大,主要的差異是載入檔案的時機。
使用 media=”print”
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> |
載入順序如下,他會在解析到 HTML 的時候就先開始載入檔案。
使用 JavaScript 載入
1 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> |
載入順序如下,他會在 onload 之後才開始載入檔案。
使用時機
不過如果直接全部都套用了上面的非同步載入的方法後,會發現… FOUC 問題又出現了,因為避開了渲染阻塞就等於回到原點,FOUC 的問題又會發生。所以實際上並不是無腦的直接改成非同步就能解決所有問題,使用非同步載入 CSS 有一個前提:非主要的樣式才使用。例如:網站使用了一些像是 fancybox 之類,第一時間看不到的套件。
大部分情況使用上面第二種方式就可以了,不過實務上有時會遇到次要的檔案太多,同時下載時,由網路頻寬問題影響了主要 CSS 的載入,例如以下模擬
使用 JavaScript 載入可以避免
拆分檔案
在理解上面的原理之後,我們知道非主要樣式才使用非同步載入,所以我們必須將檔案拆分來配合使用,例如原本都塞成一個檔案:
1 | <link rel="stylesheet" href="all.css"> |
可能拆成多個檔案
1 | <link rel="stylesheet" href="main.css"> |