CSS 渲染原理以及優化策略

  • 2019 年 11 月 23 日
  • 筆記

轉載自:http://jartto.wang/2019/10/23/css-theory-and-optimization/作者:Jartto

推薦理由:由淺入深,鞭辟入裡。

提起 CSS 很多童鞋都很不屑,尤其是看到 RedMonk 2019 Programming Language Rankings 的時候,CSS 竟然排到了第七位。

我們先來看看這張排行榜:

css渲染原理

既然 CSS 這麼重要,那麼我們花點時間來研究相關原理也就物有所值了。

本節我們就來說說 CSS 渲染以及優化相關的內容,主要圍繞以下幾點,由淺入深,了解來龍去脈:

  1. 瀏覽器構成
  2. 渲染引擎
  3. CSS 特性
  4. CSS 語法解析過程
  5. CSS 選擇器執行順序
  6. 高效的 ComputedStyle
  7. CSS 書寫順序對性能有影響嗎
  8. 優化策略

瀏覽器構成

瀏覽器構成

  • User Interface: 用戶介面,包括瀏覽器中可見的地址輸入框、瀏覽器前進返回按鈕、書籤,歷史記錄等用戶可操作的功能選項。
  • Browser engine: 瀏覽器引擎,可以在用戶介面和渲染引擎之間傳送指令或在客戶端本地快取中讀寫數據,是瀏覽器各個部分之間相互通訊的核心。
  • Rendering engine: 渲染引擎,解析 DOM 文檔和 CSS 規則並將內容排版到瀏覽器中顯示有樣式的介面,也就是排版引擎,我們常說的瀏覽器內核主要指的就是渲染引擎。
  • Networking: 網路功能模組,是瀏覽器開啟網路執行緒發送請求以及下載資源的模組。
  • JavaScript Interpreter: JS 引擎,解釋和執行 JS 腳本部分,例如 V8 引擎。
  • UI Backend: UI 後端則是用於繪製基本的瀏覽器窗口內控制項,比如組合選擇框、按鈕、輸入框等。
  • Data Persistence: 數據持久化存儲,涉及 Cookie、LocalStorage 等一些客戶端存儲技術,可以通過瀏覽器引擎提供的 API 進行調用。

渲染引擎

渲染引擎,解析 DOM 文檔和 CSS 規則並將內容排版到瀏覽器中顯示有樣式的介面,也就是排版引擎,我們常說的瀏覽器內核主要指的就是渲染引擎。

渲染引擎

上圖中,我們需要關注兩條主線:

  • 其一,HTML Parser 生成的 DOM 樹;
  • 其二,CSS Parser 生成的 Style Rules(CSSOM 樹)

在這之後,DOM 樹與 Style Rules 會生成一個新的對象,也就是我們常說的 Render Tree 渲染樹,結合 Layout 繪製在螢幕上,從而展現出來。

CSS 特性

1.優先順序

css優先順序

選擇器

權重

!important

1/0(無窮大)

內聯樣式

1000

ID

100

類/偽類/屬性

10

元素/偽元素

1

通配符/子選擇器/相鄰選擇器

0

!important > 行內樣式(權重1000) > ID 選擇器(權重 100) > 類選擇器(權重 10) > 標籤(權重1) > 通配符 > 繼承 > 瀏覽器默認屬性

示例程式碼一:

<div >    <p id="box" class="text">Jartto's blog</p>  </div>  <style>    #box{color: red;}    .text{color: yellow;}  </style>

猜一猜,文本會顯示什麼顏色?當你知道 「ID 選擇器 > 類選擇器 」的時候,答案不言自明。

升級一下:

<div id="box">    <p class="text">Jartto's blog</p>  </div>  <style>    #box{color: red;}    .text{color: blue;}  </style>

這裡就考查到了規則「類選擇器 > 繼承」,ID 對文本來說是繼承過來的屬性,所以優先順序不如直接作用在元素上面的類選擇器。

2.繼承性

  • 繼承得到的樣式的優先順序是最低的,在任何時候,只要元素本身有同屬性的樣式定義,就可以覆蓋掉繼承值。
  • 在存在多個繼承樣式時,層級關係距離當前元素最近的父級元素的繼承樣式,具有相對最高的優先順序。

有哪些屬性是可以繼承的呢,我們簡單分一下類:

  1. font-familyfont-sizefont-weightf 開頭的 CSS 樣式。
  2. text-aligntext-indentt 開頭的樣式。
  3. color

詳細的規則,請看下圖:

css繼承性

示例程式碼二:

<div>    <ol>      <li> Jartto's blog </li>    </ol>  </div>    <style>    div { color : red!important; }    ol { color : green; }  </style>

增加了 !important,猜一猜,文本顯示什麼顏色?

3.層疊性

css層疊性

層疊就是瀏覽器對多個樣式來源進行疊加,最終確定結果的過程。

CSS 之所以有「層疊」的概念,是因為有多個樣式來源。

CSS 層疊性是指 CSS 樣式在針對同一元素配置同一屬性時,依據層疊規則(權重)來處理衝突,選擇應用權重高的 CSS 選擇器所指定的屬性,一般也被描述為權重高的覆蓋權重低的,因此也稱作層疊。

示例程式碼三:

<div >    <p class="two one">Jartto's blog</p>  </div>  <style>    .one{color: red;}    .two{color: blue;}  <style>

如果兩個類選擇器同時作用呢,究竟以誰為準?這裡我們要考慮樣式表中兩個類選擇器的先後順序,後面的會覆蓋前面的,所以文本當然顯示藍色了。

升級程式碼:

<div>    <div>      <div>Jartto's blog</div>    </div>  </div>    <style>    div div div { color: green; }    div div { color: red; }    div { color: yellow; }  <style>

這個比較直接,算一下權重,誰大聽誰的。

繼續升級:

<div id="box1" class="one">    <div id="box2" class="two">      <div id="box3" class="three"> Jartto's blog </div>    </div>  </div>  <style>    .one .two div { color : red; }    div #box3 { color : yellow; }    #box1 div { color : blue; }  </style>

權重:

0 0 2 1  0 1 0 1  0 1 0 1

驗證一下:

<div id="box1" class="one">    <div id="box2" class="two">      <div id="box3" class="three"> Jartto's blog </div>    </div>  </div>  <style>  .one .two div { color : red; }  #box1 div { color : blue; }  div .three { color : green; }  </style>

權重:

0 0 2 1  0 1 0 1  0 0 1 1

如果你對上面這些問題都了如指掌,那麼恭喜你,基礎部分順利過關,可以繼續升級了!

CSS 語法解析過程

1.我們來把 CSS 拎出來看一下,HTML Parser 會生成 DOM 樹,而 CSS Parser 會將解析結果附加到 DOM 樹上,如下圖:

css

2.CSS 有自己的規則,一般如下:WebKit 使用 FlexBison 解析器生成器,通過 CSS 語法文件自動創建解析器。Bison 會創建自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。

這兩種解析器都會將 CSS 文件解析成 StyleSheet 對象,且每個對象都包含 CSS 規則。CSS 規則對象則包含選擇器和聲明對象,以及其他與 CSS 語法對應的對象。

css語法解析

3.CSS 解析過程會按照 RuleDeclaration 來操作:

css語法解析

4.那麼他是如何解析的呢,我們不妨列印一下 CSS Rules

控制台輸入:

document.styleSheets[0].cssRules

css語法解析

列印出來的結果大致分為幾類:

  • cssText:存儲當前節點規則字元串
  • parentRule:父節點的規則
  • parentStyleSheet:包含 cssRules,ownerNode,rules 規則

規則貌似有點看不懂,不用著急,我們接著往下看。

5.CSS 解析和 Webkit 有什麼關係?

css語法解析

CSS 依賴 WebCore 來解析,而 WebCore 又是 Webkit 非常重要的一個模組。

要了解 WebCore 是如何解析的,我們需要查看相關源碼:

CSSRule* CSSParser::createStyleRule(CSSSelector* selector)  {      CSSStyleRule* rule = 0;      if (selector) {          rule = new CSSStyleRule(styleElement);          m_parsedStyleObjects.append(rule);          rule->setSelector(sinkFloatingSelector(selector));          rule->setDeclaration(new CSSMutableStyleDeclaration(rule, parsedProperties, numParsedProperties));      }      clearProperties();      return rule;  }

從該函數的實現可以很清楚的看到,解析器達到某條件需要創建一個 CSSStyleRule 的時候將調用該函數,該函數的功能是創建一個 CSSStyleRule,並將其添加已解析的樣式對象列表 m_parsedStyleObjects 中去,這裡的對象就是指的 Rule

注意:源碼是為了參考理解,不需要逐行閱讀!

Webkit 使用了自動程式碼生成工具生成了相應的程式碼,也就是說詞法分析和語法分析這部分程式碼是自動生成的,而 Webkit 中實現的 CallBack 函數就是在 CSSParser 中。

這時候就不得不提到 AST 了,我們繼續剖析。

補充閱讀:Webkit 對 CSS 支援

6.關於 AST

如果對 AST 還不了解,請移步 AST 抽象語法樹。這裡我們不做過多解釋,主要圍繞如何解析這一過程展開,先來看一張 Babel 轉換過程圖:

ast

我們來舉一個簡單的例子,聲明一個箭頭函數,如下:

let jarttoTest = () => {    // Todo  }

通過在線編譯,生成如下結果:

ast

從上圖我們可以看出:我們的箭頭函數被解析成了一段標準程式碼,包含了類型,起始位置,結束位置,變數聲明的類型,變數名,函數名,箭頭函數表達式等等。

標準的解析程式碼,我們可以對其進行一些加工和處理,之後通過相應 API 輸出。

很多場景都會用到這個過程,如:

  • JS 反編譯,語法解析。
  • Babel 編譯 ES6 語法。
  • 程式碼高亮。
  • 關鍵字匹配。
  • 作用域判斷。
  • 程式碼壓縮。

場景千千萬,但是都離不開一個過程,那就是:

AST 轉換過程:解析 – 轉換 – 生成

到這裡,CSS 如何解析的來龍去脈我們已經非常清楚了,可以回到文章開頭的那個流程圖了,相信你一定會有另一翻感悟。

CSS 選擇器執行順序

渲染引擎解析 CSS 選擇器時是從右往左解析,這是為什麼呢?舉個例子:

<div>     <div class="jartto">        <p><span> 111 </span></p>        <p><span> 222 </span></p>        <p><span> 333 </span></p>        <p><span class='yellow'> 444 </span></p>     </div>  </div>    <style>    div > div.jartto p span.yellow {     color: yellow;    }  </style>

我們按照「從左到右」的方式進行分析:

  1. 先找到所有 div 節點。
  2. div 節點內找到所有的子 div,並且是 class = 「jartto」
  3. 然後再依次匹配 p span.yellow 等情況。
  4. 遇到不匹配的情況,就必須回溯到一開始搜索的 div 或者 p 節點,然後去搜索下個節點,重複這樣的過程。

這樣的搜索過程對於一個只是匹配很少節點的選擇器來說,效率是極低的,因為我們花費了大量的時間在回溯匹配不符合規則的節點。

我們按照「從右向左」的方式進行分析:

  1. 首先就查找到 class=「yellow」span 元素。
  2. 接著檢測父節點是否為 p 元素,如果不是則進入同級其他節點的遍歷,如果是則繼續匹配父節點滿足 class=「jartto」div 容器。
  3. 這樣就又減少了集合的元素,只有符合當前的子規則才會匹配再上一條子規則。

綜上所述,我們可以得出結論:

瀏覽器 CSS 匹配核心演算法的規則是以從右向左方式匹配節點的。

這樣做是為了減少無效匹配次數,從而匹配快、性能更優。

所以,我們在書寫 CSS Selector 時,從右向左的 Selector Term 匹配節點越少越好。

不同 CSS 解析器對 CSS Rules 解析速度差異也很大,感興趣的童鞋可以看看 CSS 解析引擎,這裡不再贅述。

高效的 ComputedStyle

瀏覽器還有一個非常棒的策略,在特定情況下,瀏覽器會共享 Computed Style,網頁中能共享的標籤非常多,所以能極大的提升執行效率!

如果能共享,那就不需要執行匹配演算法了,執行效率自然非常高。

如果兩個或多個 ElementComputedStyle 不通過計算可以確認他們相等,那麼這些 ComputedStyle 相等的 Elements 只會計算一次樣式,其餘的僅僅共享該 ComputedStyle

<section class="one">      <p class="desc">One</p>  </section>    <section class="one">      <p class="desc">two</p>  </section>

如何高效共享 Computed Style ?

  1. TagNameClass 屬性必須一樣。
  2. 不能有 Style 屬性。哪怕 Style 屬性相等,他們也不共享。3.不能使用 Sibling selector,譬如: first-child:last-selector+ selector。4.mappedAttribute 必須相等。

為了更好的說明,我們再舉兩個例子:

不能共享,上述規則 2

<p style="color:red">jartto's</p>  <p style="color:red">blog</p>

可以共享,上述規則 4

<p align="middle">jartto's</p>  <p align="middle">blog</p>

到這裡,相信你對 ComputedStyle 有了更多的認識,程式碼也就更加精鍊和高效了。

CSS 書寫順序對性能有影響嗎?

需要注意的是:瀏覽器並不是一獲取到 CSS 樣式就立馬開始解析,而是根據 CSS 樣式的書寫順序將之按照 DOM 樹的結構分布渲染樣式,然後開始遍歷每個樹結點的 CSS 樣式進行解析,此時的 CSS 樣式的遍歷順序完全是按照之前的書寫順序。

在解析過程中,一旦瀏覽器發現某個元素的定位變化影響布局,則需要倒回去重新渲染。

我們來看看下面這個程式碼片段:

width: 150px;  height: 150px;  font-size: 24px;  position: absolute;

當瀏覽器解析到 position 的時候突然發現該元素是絕對定位元素需要脫離文檔流,而之前卻是按照普通元素進行解析的,所以不得不重新渲染。

渲染引擎首先解除該元素在文檔中所佔位置,這就導致了該元素的佔位情況發生了變化,其他元素可能會受到它迴流的影響而重新排位。

我們對程式碼進行調整:

position: absolute;  width: 150px;  height: 150px;  font-size: 24px;

這樣就能讓渲染引擎更高效的工作,可是問題來了:

在實際開發過程中,我們如何能保證自己的書寫順序是最優呢?

這裡有一個規範,建議順序大致如下:

  1. 定位屬性 position display float left top right bottom overflow clear z-index
  2. 自身屬性 width height padding border margin background
  3. 文字樣式 font-family font-size font-style font-weight font-varient color
  4. 文本屬性 text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
  5. CSS3 中新增屬性 content box-shadow border-radius transform

當然,我們需要知道這個規則就夠了,剩下的可以交給一些插件去做,譬如 CSSLint(能用程式碼實現的,千萬不要去浪費人力)。

優化策略

我們從瀏覽器構成,聊到了渲染引擎,再到 CSS 的解析原理,最後到執行順序,做了一系列的探索。期望大家能從 CSS 的渲染原理中了解整個過程,從而寫出更高效的程式碼。

1. 使用 id selector 非常的高效

在使用 id selector 的時候需要注意一點:因為 id 是唯一的,所以不需要既指定 id 又指定 tagName

/* Bad  */  p#id1 {color:red;}    /* Good  */  #id1 {color:red;}

2. 避免深層次的 node

譬如:

/* Bad  */  div > div > div > p {color:red;}  /* Good  */  p-class{color:red;}

3. 不要使用 attribute selector

如:p[att1=」val1」],這樣的匹配非常慢。更不要這樣寫:p[id="id1"],這樣將 id selector 退化成 attribute selector

/* Bad  */  p[id="jartto"]{color:red;}  p[class="blog"]{color:red;}  /* Good  */  #jartto{color:red;}  .blog{color:red;}

4. 將瀏覽器前綴置於前面,將標準樣式屬性置於最後

類似:

.foo {    -moz-border-radius: 5px;    border-radius: 5px;  }

可以參考這個 Css 規範。

5. 遵守 CSSLint 規則

font-faces                 不能使用超過5個web字體  import                    禁止使用@import  regex-selectors              禁止使用屬性選擇器中的正則表達式選擇器  universal-selector           禁止使用通用選擇器*  unqualified-attributes       禁止使用不規範的屬性選擇器  zero-units                  0後面不要加單位  overqualified-elements       使用相鄰選擇器時,不要使用不必要的選擇器  shorthand                 簡寫樣式屬性  duplicate-background-images    相同的url在樣式表中不超過一次

6. 減少 CSS 文檔體積

  • 移除空的 CSS 規則(Remove empty rules)。
  • 值為 0 不需要單位。
  • 使用縮寫。
  • 屬性值為浮動小數 0.xx,可以省略小數點之前的 0
  • 不給 h1-h6 元素定義過多的樣式。

7. CSS Will Change

WillChange 屬性,允許作者提前告知瀏覽器的默認樣式,使用一個專用的屬性來通知瀏覽器留意接下來的變化,從而優化和分配記憶體。

8. 不要使用 @import

使用 @import 引入 CSS 會影響瀏覽器的並行下載。

使用 @import 引用的 CSS 文件只有在引用它的那個 CSS 文件被下載、解析之後,瀏覽器才會知道還有另外一個 CSS 需要下載,這時才去下載,然後下載後開始解析、構建 Render Tree 等一系列操作。

多個 @import 會導致下載順序紊亂。在 IE 中,@import 會引發資源文件的下載順序被打亂,即排列在 @import 後面的 JS 文件先於 @import 下載,並且打亂甚至破壞 @import 自身的並行下載。

9. 避免過分迴流/重排(Reflow

瀏覽器重新計算布局位置與大小。

常見的重排元素:

width  height  padding  margin  display  border-width  border  top  position  font-size  float  text-align  overflow-y  font-weight  overflow  left  font-family  line-height  vertical-align  right  clear  white-space  bottom  min-height

10. 高效利用 computedStyle

  • 公共類。
  • 慎用 ChildSelector
  • 儘可能共享。

更多請查看上文 – 高效的 ComputedStyle

11. 減少昂貴屬性

當頁面發生重繪時,它們會降低瀏覽器的渲染性能。所以在編寫 CSS 時,我們應該盡量減少使用昂貴屬性,如:

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

12. 依賴繼承

如果某些屬性可以繼承,那麼自然沒有必要在寫一遍。

13. 遵守 CSS 順序規則

上面就是對本文的一個總結,你了解 CSS 具體的實現原理,曉得規避錯誤書寫方式,知道為什麼這麼優化,這就夠了。

性能優化,進無止境。

文章首發於 Jartto's blog