近期在專案上開始需要針對部分頁面提升其效能分數,因此開始研究如何提升前端效能,希望可以持續在不影響頁面既有功能及樣式的情況下,給使用者帶來更快速且舒適的體驗。
在前端開發上,我們透過撰寫 CSS 樣式來美化我們的頁面。隨著頁面越來越複雜,所需的 CSS 檔案容量越來越大,就會讓阻塞渲染的時間越來越長,進而影響到使用者實際看到畫面的時間。
什麼是阻塞渲染?
在瀏覽器取得頁面後,會開始建構 DOM 及 CSSOM 並合併成 Render Tree。在瀏覽器建構 CSSOM 時,瀏覽器會載入並解析頁面所需的 CSS,此時瀏覽器的渲染流程便會被阻塞,直到 CSS 加載並解析完成。
為什麼瀏覽器會在所需的 CSS 準備完成前阻塞渲染呢?
如果所需的 CSS 在準備完成前,瀏覽器就直接渲染了畫面,使用者首先看到的就會是完全沒有樣式的畫面,過了一下子又變成有樣式的,這個狀況被稱為「內容樣式短暫失效(FOUC)」。
在了解如何減少阻塞渲染的時間前,我們可以先簡單了解一下 <link>
這個 HTML 元素的用途。
<link>
<link>
元素最常被使用於 CSS 樣式表,例如:
<link href="/media/examples/link-element-example.css" rel="stylesheet">
較常使用的為 href
, rel
這兩種屬性,rel
代表了關係(relationship),我自己把它解讀為「在這個網站中作為什麼被使用」;href
則定義了其資源所在的路徑。
以 rel 來說,較常使用的值為:
- stylesheet — 樣式表
- icon — 網站圖示
- apple-touch-icon — 在 iOS 裝置下將頁面加入主畫面時,顯示的圖示(如下圖)
- preload — 表示瀏覽器應預加載其資源
什麼意思呢?rel=”preload”
會告訴瀏覽器「請盡速下載並快取這份資源」。下載完後,瀏覽器不會對其做任何事情,因此我們需要用到 as
告訴瀏覽器「請將這份資源作為_來解析」。在下載完成後,再透過 onload
屬性告訴瀏覽器「在完成下載後,請將 rel
的值改為 stylesheet
」。例如:
<link rel="preload" href="css/style.css" as="style" onload="this.rel = 'stylesheet'" />
因為 rel=”preload”
載入的資源不會導致阻塞渲染,因此可以藉此實現「非同步載入 CSS」的效果,在載入 CSS 的同時,頁面的渲染工作也持續進行著。
若實際在 CSS 中使用,並開啟該頁面時,會發現瀏覽器先出現了僅有 HTML 的畫面,接著才出現套用過樣式的畫面,也就是最一開始所說的 FOUC。
我們能做的,就是將樣式拆分為 critical 及 non-critical,讓使用者進入網頁時會馬上看到區塊所需的 CSS 阻塞渲染,讓其餘非必要在渲染前就解析的 CSS 採非同步載入,例如:
<!-- 瀏覽器會載入此 CSS 直到完成後才繼續渲染 -->
<link rel="stylesheet" href="critical.css" /><!-- 瀏覽器非同步載入的CSS -->
<link rel="preload" href="less-critical.css" as="style" onload="this.rel = 'stylesheet'" />
雖然 preload 使用上非常直觀,如果專案在支援度上需要概括 IE,就無法使用 preload 來實現非同步載入 CSS。在這樣的狀況下,我們可以考慮另一個辦法。
透過替換 media 屬性實現
當瀏覽器在解析頁面發現 stylesheet
時,會下載其 href
位址的資源。然而,若該 link
元素明確表示其使用情境時,在非該情境的狀況下,就不會對頁面渲染造成阻塞。
使用的方式很簡單:
<link rel="stylesheet" href="less-critical.css" media="print" onload="this.media='all'" />
這個 link
元素告訴瀏覽器:「這份 CSS 是列印頁面時才會用到,請在不停止渲染階段的情況下載入。」並且在載入完成後將 media
調整為 all
,讓瀏覽器在所有情境中使用它。
可以使用純 JavaScript 實現這件事情嗎?
可以!做法就像我們手動掛載 script
一樣:
// 建立 link 元素
const mainCSS = document.createElement("link");
mainCSS.rel = "stylesheet";
mainCSS.href = "css/style.css";
// 將其置入 head 最末端
document.head.insertBefore(
mainCSS,
document.head.childNodes[document.head.childNodes.length - 1]
.nextSibling
);
但我相信絕大多數情境應該都是可以用 link
單純地達成需求。
關於非同步載入 CSS 的筆記就記錄到此,如果內文有任何錯誤,也煩請不吝指出,希望可以幫助到一些人,謝謝大家。
References: