開始之前的心理建設#
瀏覽器解析 CSS 的速度其實非常快,所以本章談的技巧,效能收益通常不會像減少網路請求、縮小 JavaScript bundle 或圖片最佳化那麼顯著。撰寫 CSS 第一原則仍是「可讀性與可維護性」。
不過「不會有巨大影響」不代表可以隨便寫。在大型專案累積之下,糟糕的 CSS 也能讓頁面渲染慢上幾個檔次。把幾個關鍵原則記在腦中,邊寫邊套用,幾乎沒有額外成本。
CSS 選取器(Selector)的匹配方向#
許多人以為 CSS 選取器(Selector)是「從左到右」匹配,但實際上瀏覽器是從右到左比對的。例如:
#ironman .article h2 {
/* some style */
}瀏覽器會先找出頁面所有 h2,再逐層往上過濾:祖先有 .article 嗎?再上去有 #ironman 嗎?整個流程比直接 #ironman-article-h2 多了好幾倍的檢查工作。
理解這點之後,再看不同選取器的效能差異就會很合理。常見選取器由快到慢大致是:
- ID Selector
- Class Selector
- Element Selector
- General Sibling Combinator
- Child Combinator
- Descendant Combinator
- Attribute Selector
- Pseudo Element/Class Selector
ID 與 Class Selector 的實際效能差異很小,現代專案多以 Class 為主,沒必要硬塞 ID。
CSS Selector 的撰寫建議#
不要濫用萬用字元 *#
#ironman * {
color: yellow;
}由於是從右往左匹配,* 會先把頁面上所有元素抓進來再過濾,是相當昂貴的操作。SQL 中的 SELECT * 之所以被勸退,背後是同樣的道理。最右側的選取器又稱「關鍵選取器」,它的成本決定了整條選取器的成本。
不必對 ID 多加標籤限定#
div#navbar {
}
p.ironman {
}#navbar 本身就唯一,前面再加上 div 等於多做一次比對。直接寫 #navbar 即可。
子選取器優於後代選取器#
div > p 只檢查一層父子關係;div p 則要逐層往上找 div,成本明顯較高。在語意正確的前提下,能用子選取器就用子選取器。
善用屬性繼承#
CSS 屬性會從父元素繼承到子元素,沒必要每個子選取器都重寫一次:
/* 不建議 */
#ironman {
}
#ironman > .kyle {
color: blue;
}
#ironman > .mo {
color: blue;
}
/* 建議 */
#ironman {
color: blue;
}留意昂貴的 CSS 屬性#
某些屬性需要瀏覽器額外計算或合成,例如:
:nth-childfilteropacitybox-shadowborder-radius
這些屬性本身都很常見,並不是要禁用,而是當需求可以用其他方式達成時,先想想是否真的必要。例如 transition 寫法,明確指定屬性會比 all 好:
/* 不建議 */
.ironman {
scale: 1;
transition: all 0.2s ease-in;
}
/* 建議 */
.ironman {
scale: 1;
transition: scale 0.2s ease-in;
}transition: all 會讓瀏覽器多做一些屬性判斷。
減少 reflow 與 repaint#
要讓 CSS 表現好,必須先理解這兩個流程:
- reflow:改動會讓瀏覽器重新計算 layout、重建 Render Tree。
- repaint:layout 不變但畫面顏色或紋理變動。
哪些操作會觸發 reflow#
- 改動
margin、padding - 改動
width、height、top、left等位置屬性 - 改動
font-size、font-family - 改變視窗大小
要做動畫時優先考慮 transform,因為它只觸發 compositing,而改 margin 會走完 reflow + repaint + compositing 的整條鏈。佈局時 flex 在 reflow 上的表現也比 inline-block 與 float 好。
避免不必要的 repaint#
repaint 比 reflow 容易發生,幾乎所有顏色、背景、邊框變動都會觸發。避免方式:
- 一次讀完所需的版面資訊,再一次寫回,讓瀏覽器合併成一次 reflow。
- 用「新增 / 移除 class」取代多行 inline style 設定。
- 滾動時暫時關閉 hover:
.disable-hover,
.disable-hover * {
pointer-events: none !important;
}let timer;
window.addEventListener(
"scroll",
() => {
document.body.classList.add("disable-hover");
clearTimeout(timer);
timer = setTimeout(() => {
document.body.classList.remove("disable-hover");
}, 500);
},
false
);想知道哪些屬性會觸發 reflow / repaint,可以查 CSS Triggers ↗ 這個整理表格。
Critical CSS#
CSS 是 render-blocking resource,下載與解析完畢前頁面無法呈現。Critical CSS 的做法是:把首屏(above-the-fold)所需要的關鍵樣式抽出來,直接內嵌(inline)到 HTML 的 <style> 中,避免額外請求。
優點:縮短 First Contentful Paint。
限制:
- 內嵌 CSS 不會被瀏覽器快取,整段 inline 會跟著 HTML 一起重複下載。
- 若 critical CSS 太大,會佔掉 TCP slow-start 的初始 14 KB 額度,反而拖慢 HTML 後續部分。
實務上可借助 npm 套件如 critical、criticalCSS、pentHouse,搭配 gulp 或 Webpack 整合到 build flow,並用 Lighthouse 觀察是否真的有 render-blocking resource 問題再決定要不要做。
Preload 搭配 Critical CSS#
<link rel="preload"> 可以提早觸發資源下載,常見搭配場景:
- Preload CSS 中指定的關鍵 background-image:原本要等 CSS parse 完才會發現它的存在,預先下載可以省下這段等待。
- Preload Non-Critical CSS:當頁面已採用 critical inlined CSS,剩下的非關鍵 CSS 可以視需求 preload,避免阻塞渲染又能盡早就位。
小結#
CSS 帶來的效能差距通常小於 JavaScript 與圖片,但仍有不少可以累積的優化空間。把握以下幾個重點即可:
- CSS 選取器是「從右到左」匹配,撰寫時讓最右側盡可能精準。
- 避免無謂的 reflow;動畫優先用 transform。
- 思考是否需要 Critical CSS,並用 Lighthouse 驗證效益。
理解這些原則,平時寫 CSS 自然會做出對效能友善的選擇。