iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

今晚,我想來點 Web 前端效能優化大補帖!系列 第 12

Day12 X Writing High Performance CSS

CSS 是前端開發者不可不學的技術之一,沒有了它就好像你做出來的網頁都沒有穿衣服一樣,有點羞於見人呀!?

但你有想過 CSS 如果亂寫也可能會影響到網頁的效能嗎?
叮咚!您今天的晚餐送來囉!是一份 Writing High Performance CSS 的大補帖!一起來看看會影響頁面效能的 CSS 的眉眉角角吧 ?

在開始之前

其實瀏覽器解析 CSS 的速度非常快,所以今天所說的技巧帶來的效能收益可能沒有你想的那麼顯著。相比撰寫良好 CSS 帶來的優化,大部分時間我們應該聚焦在減少網路請求、減少 JavaScript bundle size、圖片最佳化等會非常顯著影響頁面效能或載入時間的優化技巧。

也因為 CSS 不太會對性能產生「巨大」的影響,所以在撰寫 CSS StyleSheet 時應該始終以可讀性與可維護性為原則。不過不能因為不會對性能產生「巨大」的影響,就自顧自的亂寫一通,覺得樣式有出來就好。還記得第一天建立的 mindset 嗎?

效能優化是一段追求好還要更好的過程。

學會撰寫對提升效能較有幫助的 CSS,尤其是在開發大型專案時.累積起來也可能有不少優化空間。相反的,如果一直不管品質,也許累積起來在某些情況下也會成為效能的瓶頸。

So, Why Not ?

CSS Selector

不同於我們閱讀與撰寫是從左至右的習慣,CSS Selector 的匹配是反過來「從右向左進行的」。(這點我居然到寫程式的第二年才知道啊!!)

這也導致不同的 Selector 寫法之間的效能也存在著一些差異。

例如:

#ironman .article h2 {
/*     Some Style */
}

使用這個 selector 需要先找到 DOM 中所有的 h2 元素,再 filter 掉祖先元素不是 .article 的,最後再 filter 掉祖先元素不是 #ironman 的元素。

這個 selector 相比於直接用 id selector 例如 #ironman-article-h2 會需要花更多的時間才能生成 render-tree。

不同 CSS Selector 的性能

  1. ID selector ID選擇器
  2. Class Selector 類別選擇器
  3. Element Selector 元素選擇器
  4. General Sibling Combinator 兄弟選擇器
  5. Child Combinator 子選擇器
  6. Descendant Combinator 後代選擇器
  7. Attribute Selector 屬性選擇器
  8. Pseudo Element/Class Selector 偽元素選擇器

以上是常見的選擇器,按照效能排列從上到下效能由高到低。
(註:ID Selector 與 Class Selector 效能其實差異不大)

CSS Selector 優化 Tips

我們現在已經知道 CSS Selector 是「從右到左」查找,也介紹了單個選擇器的效能比對,接著就可以來看看一些被推薦的 CSS Best Practice 寫法。

千萬不要亂用萬用字元 - *

#ironman * {
  color: yellow;
  font-size: 12px;
}

因為是「從右至左」去尋找,所以這個選擇器會先遇到萬用字元而匹配到頁面上所有的元素,這是一個成本非常大的操作。

(有學過 SQL 的應該也知道 SELECT * FROM 是很耗資料庫效能的語法)

所以一個 CSS 選擇器的效能關鍵通常都在最右側,這個最右側的選擇器又被稱作「關鍵選擇器」。

不是都用效能最好的 ID Selector 就好

前面有提到 ID Selector 的效能是最好的,「欸!那就全部都用 ID Selector 就好啦!今天可以下班囉!」等等等,這樣我們得對頁面上所有元素都指定 id attribute,況且 id 還必須是唯一不能重複的識別碼,你覺得現實中這可能嗎?嗯...看來是一個效能與最佳實踐的取捨。

不過剛剛也有提到其實 Class Selector 的效能不會比 ID Selector 差太多,所以大部分的狀況是建議使用 Class Selector,而不是 ID。不然你想想,如果是採用元件化開發,會有重複使用元件的狀況,那重複 ID 反而成為了一個問題,雖然可以動態傳入參數丟到 id attribute 裡,但並不是那麼好維護。

ID Selector 適用於標識長久存在且唯一的元素,例如頁面中的 nav bar 或 header,就適合使用 ID Selector:

#navbar {
  background-color: green;
}

避免在 ID 或是 Class Selector 前面限定元素類型

div#navbar {}
p.ironman {}

我以前並不覺得這樣的寫法有問題,不過我們用 CSS「從右至左」匹配元素的思路來分析看看。

首先看到 #navbar 而先去找到 id 為 navbar 的元素,注意,id 是不會重複的,所以照理來說已經找到我們要的元素了,不過因為發現左邊有一個 div,所以還是要額外再去分析它,當然最後結果跟直接用 #navbar 去匹配是一樣的,反而還額外做了一次判斷。

Child Combinator 子選擇器 優先於 Descendant Combinator 後代選擇器

如果今天你想找包在 div 裡面的 p tag(一層的父子關係),那麼 div>p 這個寫法的效能會比 div a 這個寫法還要好。

一樣從右邊的選擇器開始看,兩者都會先去搜尋頁面上的所有 p tag,不過前者只會檢查一個層級,看看這些 p tag 的父層有沒有是 div tag 的。後者則會一層層往上查找,父層沒有就往父層的父層查找。相比起來前者耗費的工會少許多。

善用 CSS 屬性繼承的特性

父元件的 CSS Properties 是會繼承到子元件上的,所以如果子元件要沿用父元件的樣式,建議使用繼承的特性,而不是在每個子選擇器都設定一次。

/* This is not good */
#ironman {}
#ironman > .kyle { color: blue; }
#ironman > .mo { color: blue; }

/* This is better */
#ironman { color: blue; }

非必要的情況,減少使用昂貴的 Attribute

所謂昂貴的屬性指的是一些需要瀏覽器進行操作或計算的屬性,它們需要耗費更多的瀏覽器性能。當頁面重繪時,這些屬性可能會降低瀏覽器的渲染效能。例如:

  • :nth-child
  • filter
  • opacity
  • box-shadow
  • border-radius

不過其實這些都是很常見且很重要的屬性呢,所以並不是叫大家不要使用,而是建議當你的需求有其他方式可以達成時,可以思考一下是不是真的需要使用這些屬性。

要用 CSS 來寫動畫時,通常會用 transition property 來指定哪些 CSS properties 會被轉場效果影響、transition duration、transition timing function 與 transition delay。

舉例來說你希望指定 class name 為 ironman 的元素在 hover 時 scale 會變 1.5 倍,你希望這個過程是有平滑的動畫的,你可能會這樣寫:

.ironman {
    scale: 1; 
    transition: all .2s ease-in;
}

.ironman:hover {
    scale: 1.5;
}

但其實如果你已經明確知道 transition 的對象是 scale,你應該這樣寫

/* 不要用 all */
.ironman {
    scale: 1; 
    transition: scale .2s ease-in;
}

transition: all 的寫法會讓瀏覽器在特定情況下會額外去做一些判斷,相較於 specific 的寫法效能會比較差。

優化 Reflow & Repaint

在 Day008 的時候有提到 reflowrepaint,我們知道不同的改變樣式的方式,是會觸發不同渲染流程的,因此也是效能優化的一個方向。

減少 Reflow

Reflow 會導致瀏覽器重新計算頁面元素的 Layout 並重新構建 Render Tree,這一過程會降低瀏覽器的渲染速度,因此如果非必要的狀況下我們應該避免頻繁觸發會造成 Reflow 的操作。我們來看看修改哪些常見的 CSS Properties 會造成 Reflow:

  • 改變元素的 margin 或 padding
  • 改變元素的 width, height, position 的 left 或 top
  • 改變 font-size 或 font-family
  • 改變視窗大小

當然並不是說禁用這些屬性,而是使用前可以想一下是否有別的方式可以完成需求,例如要使用 CSS 動畫移動元素位置應該使用 transform 而不是去改變 margin。在 Day008 的時候有說過 transform 只會觸發 compositing,而改變 margin 則會觸發 reflow + repaint + compositing。

不同 layout 方式的 reflow 性能也不同,flex 在 reflow 時比起 inline-block 與 float 有更好的性能,在佈局時可以優先考慮。

避免不必要 Repaint

基本上在更新頁面時 repaint 是比較難去避免的,除非像是 tarnsform 這種 Compositing Only 的屬性,不然舉凡像是改變顏色,改變背景圖片等操作雖然不用重新 layout,卻還是會經過 repaint。

不過我們可以試著去「避免」觸發不必要的 repaint,例如 Day008 有提到的一次一起讀取完再一次一起修改,因為瀏覽器底層機制的關係只會觸發一次 reflow 與之後的流程。或是避免使用多個 statement 修改 style,而是改用新增或移除 CSS class 的方式。

當使用者在滾動頁面的時候,去觸發 hover 事件是沒什麼意義的事,可以在滾動的時候禁用 hover 事件,提升 frame rate 讓頁面的滾動更為流暢。要做到這件事可以透過 CSS 的 pointer-events property。來一個簡易版 Demo:

.disable-hover,
.disable-hover * {
  pointer-events: none !important;
}
let timer;

window.addEventListener('scroll', function() {
  const bodyClassList = document.body.classList;
  
  // clear previous timeout function
  clearTimeout(timer);
    
  if (!bodyClassList.contains('disable-hover')) {
    // add the disable-hover class to the body element
    bodyClassList.add('disable-hover');
  }
        
  timer = setTimeout(function() {
    // remove the disable-hover class after a timeout of 500 millisecond
    bodyClassList.remove('disable-hover');
  }, 500);
  
}, false);

建議大家可以到 CSS Triger 這個網站逛逛,看看各個 CSS Property 會不會造成 reflow 與 repaint。

Extract Critical CSS

因為瀏覽器得下載與解析 CSS 後才能呈現頁面,所以 CSS 是一個「Render-Blocking-Resource」。如果在網路狀況不好或 CSS 檔案很肥大的狀況下,對於 CSS file 的請求可能會讓頁面的渲染時間大幅度增加。

Critical CSS (內聯首屏關鍵CSS) 是一個把 above-the-fold content(也就是頁面初次載入且在滾動之前的螢幕可視區範圍)的 CSS 提取出來並直接內聯到 HTML 的技術。

不過 above-the-fold content 的範圍並沒有一個明確的界定值,因為每個裝置的大小與螢幕尺寸都不會一樣。

所謂將 CSS 內聯到 HTML 大概是這個概念:

而不是引入外部 CSS 檔案的方式。
這麼做的好處是讓重要樣式可以不必透過額外網路請求去抓取,可以使瀏覽器開始頁面渲染的時間提前,減少 First Contentful Paint 的時間。

嗯...看起來挺好的,不過我想到了今天大家應該都深刻了解到看起來很棒的技術背後其實都躲著一些 trade off ?

Critical CSS 的限制是不適用於太肥大的 CSS,不然會延遲 HTML 其餘部分的傳輸。這部分跟 TCP 的機制有關,直接看看 web.dev 文件的說法:

New TCP connections cannot immediately use the full available bandwidth between the client and the server, they all go through slow-start to avoid overloading the connection with more data than it can carry. In this process, the server starts the transfer with a small amount of data and if it reaches the client in perfect condition, doubles the amount in the next roundtrip. For most servers, 10 packets or approximately 14 KB is the maximum that can be transferred in the first roundtrip.

另外一個缺點是 inlined CSS 沒有辦法像外部 CSS 檔案一樣被瀏覽器快取。所以 Critical CSS 在使用上必須謹慎一點,建議可以去 lighthouse 檢測看看頁面是不是有嚴重的 render-blocking-resource 的問題。

至於到底要怎麼抽離出 Critical CSS,有一些 npm module 可以使用,例如 criticalcriticalCSSpentHouse,這些 module 也有對應的 plugins 可以整合到自動化工具例如 gulp 或 webpack 的 workflow 中,有興趣的讀者再自行研究囉。

Preload

還記得 Day00 提到的 <link rel="preload" /> 嗎?是時候派上用場啦!

Preloading resources defined in CSS

像是在 CSS 中指定的 backgroumd-image 會等到瀏覽器載入且解析完 CSS 後才發現他們的存在並開始下載,但身為開發者的我們是事先知道會需要這個資源的,可以針對這個圖片先做 preload。 其餘類似的屬性也可以這麼做。

Preloading CSS files

如果採用了上面介紹的 Critical CSS 的方法,網頁的 CSS 被分成了兩個類別:

  • Critical Inlined CSS or rendering the above-the-fold content
  • Non-Critical CSS

這些 Non-Critical CSS 就可以根據自己的需求適時使用 preload 來提早開始下載的時機。

本日小結

經過今天後,有沒有覺得要寫出高效能的 CSS 也是十分不容易的啊!
我自己覺得初學者比較容易誤解的地方是 CSS 的匹配順序並不是像一般閱讀習慣的從左到右,而是「由右至左」,理解這個道理後再思考不同 Selector 的性能就會豁然開朗了!(我自己真的在學網頁開發後過了一年多才知道這件事QQ)

今天也介紹了許多對性能有幫助的 CSS 技巧,不過有些技術要不要使用真的得依照實際狀況謹慎考慮了,畢竟大多數提升性能的背後都有它們的 trade off 在。不過有些思維很值得記起來,例如在寫 CSS 時去想一下 reflow repaint 的流程,下手前先思考一下有沒有更好的解法。學會撰寫 high performance 的 CSS,累積起來也會對性能有可觀的幫助。

明天的主題嚴格上來說還是屬於 CSS 的範疇,來看看怎麼利用 GPU 加速來提升網頁的效能,我們明天見!

References & 含圖片來源

https://domhabersack.com/high-performance-css
https://domhabersack.com/css-hierarchy-matching
https://iter01.com/13467.html
https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/
https://web.dev/extract-critical-css/
https://web.dev/first-contentful-paint/


上一篇
Day11 X Lazy Loading
下一篇
Day13 X CSS GPU Acceleration
系列文
今晚,我想來點 Web 前端效能優化大補帖!30

1 則留言

0
Wei
iT邦新手 5 級 ‧ 2021-09-28 02:45:24

想問 opacity 不是 Composite 才執行的嗎,為何被定義為昂貴的 Attribute?

ref: https://cythilya.github.io/2018/07/13/critical-rendering-path/

Wei iT邦新手 5 級 ‧ 2021-09-28 03:10:23 檢舉

看起來是不同排版引擎實作效果不同,從說明來看普遍還是會 repaint
https://csstriggers.com/opacity

我要留言

立即登入留言