元素選取器(Selector)是前端日常的基本工具。無論撰寫一般的串接樣式表(CSS, Cascading Style Sheets),或編譯型的 SASS、SCSS、LESS,最終都會被編譯成一行行的 CSS 樣式屬性,交給瀏覽器解析、套用。這件事是如何實作的?

瀏覽器渲染#

先複習瀏覽器渲染步驟。

撰寫的 CSS 載入後,會被解析成 CSS 物件模型(CSSOM)Tree,與文件物件模型(DOM, Document Object Model)疊加成 Render Tree,隨後進行位置計算與渲染。CSS 屬性套用的關鍵在於:

  • 如何從 CSS 轉化成 CSSOM Tree
  • 如何將 CSSOM 套用到 DOM 上

CSSOM Tree#

當我們寫下一組 CSS 樣式例如:

#id .class h4 + p {
  ...;
}

直覺會認為瀏覽器是由左到右依序找出 #id > .class > h4 > p,但實際的解析順序是由右到左的 p > h4 > .class > #id

考慮到效能議題,由右到左的解析比由左到右優秀不少。

考慮以下的例子,假設這有這樣的 HTML:

<div id="div1">
  <div class="a">
    <div class="b">...</div>
    <div class="c">
      <div class="d">...</div>
      <div class="e">...</div>
    </div>
  </div>
  <div class="f">
    <div class="c">
      <div class="d">...</div>
    </div>
  </div>
</div>

以及這邊五條 CSS 樣式規則:

#div1 .c .d {
}
.f .c .d {
}
.a .c .e {
}
#div1 .f {
}
.c .d {
}

讓我們模擬一下,如果把 CSS 從左到右解析,透過 <div class="d"> 中的 .d 來思考,這樣的 CSSOM Tree 在套用樣式時,必須對所有的樣式規則進行檢查,確認樣式規則是否影響到 .d,最後才能確定可能會影響到 .d 的樣式規則有這三條:

  • #div1 .c .d
  • .f .c .d
  • .c .d

以此類推,每個 DOM Tree 上的元素,都必須遍盡所有的樣式規則,才可以取得個別的樣式,這樣會造成巨量冗餘的計算,進而危害效能。

反過來,如果將前述的 CSS 由右到左進行解析,我們從 <div class="d">.d 的角度來看看,由於會被樣式規則影響到的目標元素,已經全都集合在第一層了,我們不用再去遍盡整個 CSSOM Tree,而是只需要檢查 .d 以下的子屬性節點是否符合實際 DOM 結構,再將所有符合的樣式規則擷取出來,便能完成 .d 對此元素的樣式規則套用。

由右到左的解析順序,能夠將所有共用的規則路徑收攏在一起,瀏覽器進行屬性比對時無須再遍盡整顆 CSSOM Tree,減少了大量無效的比對計算。

也可以換個方式思考:在 HTML 的結構中,一個元素可以有無數個子元素,但只能有一個父元素,由子找父(由下往上)搜尋絕對是比較快的。

套用樣式#

CSSOM Tree 解析完成後,並無法直接與 DOM Tree 結合套用。

除了開發者定義的 CSS 檔,以下幾處也可能定義樣式規則:

  • HTML 的 inline style 設定
  • 瀏覽器預設值(就是 CSS reset/normalize 要覆蓋掉的事情)
  • 瀏覽器的使用者偏好設定

瀏覽器負責處理 CSS 的部分,會將前述的所有東西及 CSS 檔定義的樣式規則分別整理成個別的樣式規則組(CSS Rule Set),內容紀載了樣式規則、目標屬性等資訊。

目標屬性#

為了提升後面計算的效能,瀏覽器的 CSS 核心會依照樣式規則組中個別規則的目標屬性,將其分組存放;一共分成以下四組:

  • idRules
  • classRules
  • tagNameRules
  • universalRules

這樣在取用時,可以依照目標元素是否有這個屬性,快速篩出可能會套用的樣式。

套用規則#

最後就是套用規則啦~瀏覽器會依照下列順序及樣式規則權重套用所有樣式規則:

  1. 瀏覽器的預設值
  2. 瀏覽器的使用者偏好設定
  3. 開發者定義的 CSS
  4. inline style
  5. 有加上 !important 的樣式屬性

最後兩項有沒有很眼熟?這就是 CSS 樣式權重中,inline style 及 !important 輾壓樣式規則群雄背後的原因!

可能會有讀者好奇:為什麼 inline style 不同於開發者定義的 CSS,會被另外處理?

我們可以回顧一下瀏覽器渲染的步驟,由於 inline style 是存在於 DOM 元素中,只有在要將 CSS 套用到 DOM 上時才會接觸到,無法事前將兩者結合計算。

CSS 效能#

快速看過了 CSS 選取器從解析到套用的整個過程,想想日常生活中所使用的各大網站,背後都有這種龐大繁複的運算機制,是不是覺得很恐怖?

如同昨天的渲染過程,實際上瀏覽器在這一段也做了最佳化機制;瀏覽器會自動將狀態一樣的元素做樣式快取。什麼是狀態一樣呢?要滿足以下幾個條件:

  • 無設定 id
  • tagclass 須完全一致
  • 無設定 style 屬性
  • 樣式規則中不能使用各種 sibling selector(~+:first-child…)

由於上面的條件,以及前面討論到的 CSS 運算過程,撰寫 CSS 時也有幾個地方可以稍微留心一下:

  • 由於樣式規則的目標屬性會分組存放,id 選取器效能非常高,前提是不能與其他條件混用。
  • 不要寫過深的 CSS 樣式規則。
  • 能不用 inline style 就不要用,除了難維護之外,由於是存在於 DOM Tree 上,無法事先與其他樣式合併計算,效能也會大打折扣。

若能注意到諸如此類的小細節,CSS 效能自然也就能大幅提升。

延伸閱讀#

認識了 CSS 選取器之後,一定有讀者好奇,JavaScript 的元素選取器又是怎麼一回事呢?可以參考 jQuery 的 Source Code,是由左到右的解析。至於為什麼會不一樣呢,其實在文中也有答案,就留給讀者您思考挖掘。

結語#

三天的 CSS 深度之旅告一段落。理解基礎原理後,許多原本玄妙的 CSS 行為都變得清晰可解。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10218823