CSS 渲染原理以及優化策略

推薦理由:文章由淺入深,獲益匪淺。css

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

咱們先來看看這張排行榜:node

css渲染原理

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

本節咱們就來講說 CSS 渲染以及優化相關的內容,主要圍繞如下幾點,由淺入深,瞭解前因後果:git

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

瀏覽器構成

瀏覽器構成

  • User Interface:

    用戶界面,包括瀏覽器中可見的地址輸入框、瀏覽器前進返回按鈕、書籤,歷史記錄等用戶可操做的功能選項。github

  • Browser engine:

    瀏覽器引擎,能夠在用戶界面和渲染引擎之間傳送指令或在客戶端本地緩存中讀寫數據,是瀏覽器各個部分之間相互通訊的核心。web

  • 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

相關文章
相關標籤/搜索