本篇文章說明無樣式內容閃爍 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 會有一瞬間看到
FOUC

當樣式載入後才會看到正確的結果
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
2
<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'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"></noscript>

原理是利用 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
2
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"></noscript>

原理是利用 JavaScript onload 事件發生時,才將替換成條件改為可顯示的類型。一開始使用的 print 是列印的時候套用,所以正常進入網頁的時候不會生效。如果是其他 media,除了 print 可以直接用,其他一樣寫在 onload 裏面

1
2
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="desktop.css" media="print" onload="this.media='screen and (min-width:768px)'">

使用 JavaScript 載入

可以使用套件 loadCss 或者自己寫,加在頁面載入完成之後:

1
2
3
4
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css';
document.head.appendChild(link);

差異

上面三個方法的差異,前兩個差異不大,一般說 preload 的瀏覽器支援程度比差,所以建議使用第二種。而使用 JavaScript 載入,和前兩個差異就比較大,主要的差異是載入檔案的時機。

使用 media=”print”

1
2
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css" media="print" onload="this.media='all'">

載入順序如下,他會在解析到 HTML 的時候就先開始載入檔案。
載入順序

使用 JavaScript 載入

1
2
3
4
5
6
7
8
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
...
<script>
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css';
document.head.appendChild(link);
</script>

載入順序如下,他會在 onload 之後才開始載入檔案。
載入順序

使用時機

不過如果直接全部都套用了上面的非同步載入的方法後,會發現… FOUC 問題又出現了,因為避開了渲染阻塞就等於回到原點,FOUC 的問題又會發生。所以實際上並不是無腦的直接改成非同步就能解決所有問題,使用非同步載入 CSS 有一個前提:非主要的樣式才使用。例如:網站使用了一些像是 fancybox 之類,第一時間看不到的套件。

大部分情況使用上面第二種方式就可以了,不過實務上有時會遇到次要的檔案太多,同時下載時,由網路頻寬問題影響了主要 CSS 的載入,例如以下模擬
網路頻寬

使用 JavaScript 載入可以避免
網路頻寬

拆分檔案

在理解上面的原理之後,我們知道非主要樣式才使用非同步載入,所以我們必須將檔案拆分來配合使用,例如原本都塞成一個檔案:

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

可能拆成多個檔案

1
2
3
4
<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="desktop.css" media="print" onload="this.media='screen and (min-width:768px)'">
<link rel="stylesheet" href="mobile.css" media="print" onload="this.media='screen and (max-width:767px)'">