提起 css 很多童鞋都很不屑,尤其是看到 RedMonk 2019 Programming Language Rankings 的時候,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 進行調用。
二、渲染引擎
三、CSS 特性
1. 優先級 :

!important > 行內樣式(權重1000) > ID 選擇器(權重 100) > 類選擇器(權重 10) > 標簽(權重1) > 通配符 > 繼承 > 瀏覽器默認屬性
示例代碼一: https://jsfiddle.net/a5xtdoq7/1/
<div > <p id="box" class="text">Jartto's blog</p> </div>
樣式規則:
#box{color: red;} .text{color: yellow;}
升級一下: https://jsfiddle.net/a5xtdoq7/3/
<div id="box"> <p class="text">Jartto's blog</p> </div>
樣式規則:
#box{color: red;} .text{color: blue;}
2.繼承性
- 繼承得到的樣式的優先級是最低的,在任何時候,只要元素本身有同屬性的樣式定義,就可以覆蓋掉繼承值;
- 在存在多個繼承樣式時,層級關系距離當前元素最近的父級元素的繼承樣式,具有相對最高的優先級;
有哪些屬性是可以 繼承 的呢,我們簡單分一下類:
1.font-family,font-size,font-weight 等 f 開頭的 CSS 樣式; 2.text-align,text-indent 等 t 開頭的樣式; 3.color;
詳細的規則,請看下圖:

示例代碼二: https://jsfiddle.net/a5xtdoq7/
<div> <ol> <li> Jartto's blog </li> </ol> </div>
樣式規則定義如下:
div { color : red!important; } ol { color : green; }
3. 層疊性

層疊就是瀏覽器對多個樣式來源進行疊加,最終確定結果的過程。CSS 之所以有「層疊」的概念,是因為有多個樣式來源。
CSS 層疊性是指 CSS 樣式在針對同一元素配置同一屬性時, 依據層疊規則(權重)來處理沖突 ,選擇應用權重高的 CSS 選擇器所指定的屬性,一般也被描述為權重高的覆蓋權重低的,因此也稱作層疊。
示例代碼三: https://jsfiddle.net/a5xtdoq7/2/
<div > <p class="two one">Jartto's blog</p> </div>
樣式規則如下:
.one{color: red;} .two{color: blue;}
升級代碼: https://jsfiddle.net/a5xtdoq7/6/
<div> <div> <div>Jartto's blog</div> </div> </div>
樣式規則:
div div div { color: green; } div div { color: red; } div { color: yellow; }
繼續升級: https://jsfiddle.net/a5xtdoq7/7/
<div id="box1" class="one"> <div id="box2" class="two"> <div id="box3" class="three"> Jartto's blog </div> </div> </div>
樣式:
.one .two div { color : red; } div #box3 { color : yellow; } #box1 div { color : blue; }
權重:
驗證一下: https://jsfiddle.net/a5xtdoq7/9/
<div id="box1" class="one"> <div id="box2" class="two"> <div id="box3" class="three"> Jartto's blog </div> </div> </div>
樣式如下:
.one .two div { color : red; } #box1 div { color : blue; } div .three { color : green; }
四、CSS 語法解析過程
1.我們來把 CSS 拎出來看一下, html Parser 會生成 DOM 樹,而 CSS Parser 會將解析結果附加到 DOM 樹上,如下圖:

2. CSS 有自己的規則,一般如下:

3. CSS 解析過程會按照 Rule , Declaration 來操作:

4.那么他是如何解析的呢,我們不妨打印一下 CSS Rules :
控制台輸入:
document.styleSheets[0].cssRules
打印出來的結果大致分為幾類:
- cssText:存儲當前節點規則字符串
- parentRule:父節點的規則
- parentStyleSheet:包含 cssRules,ownerNode,rules 規則
規則貌似有點看不懂,不用着急,我們接着往下看。
5.CSS 解析和 Webkit 有什么關系?

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 轉換過程圖:
我們來舉一個簡單的例子,聲明一個箭頭函數,如下:
let jarttoTest = () => { // Todo }
通過 在線編譯 ,生成如下結果:

從我們可以看出:我們的箭頭函數被解析成了一段標准代碼,包含了類型,起始位置,結束位置,變量聲明的類型,變量名,函數名,箭頭函數表達式等等。
標准的解析代碼,我們可以對其進行一些加工和處理,之后通過相應 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>
樣式規則如下:
div > div.jartto p span.yellow{ color:yellow; }
我們按照「從左到右」的方式進行分析:
1.先找到所有 div 節點;
2.在 div 節點內找到所有的子 div ,並且是 class = “jartto” ;
3.然后再依次匹配 p span.yellow 等情況;
4.遇到不匹配的情況,就必須回溯到一開始搜索的 div 或者 p 節點,然后去搜索下個節點,重復這樣的過程。
有沒有覺得很低效,那么問題出在哪里?
這樣的搜索過程對於一個只是匹配很少節點的選擇器來說,效率是極低的,因為我們花費了大量的時間在 回溯匹配不符合規則的節點 。
我們不妨換個思路,還以上面的示例為准:
<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>
樣式規則如下:
div > div.jartto p span.yellow{ color:yellow; }
我們按照「從右向左」的方式進行分析:
1.首先就查找到 class=“yellow” 的 span 元素;
2.接着檢測父節點是否為 p 元素,如果不是則進入同級其他節點的遍歷,如果是則繼續匹配父節點滿足 class=“jartto” 的 div 容器;
3.這樣就又減少了集合的元素,只有符合當前的子規則才會匹配再上一條子規則。
綜上所述,我們可以得出結論:
瀏覽器 CSS 匹配核心算法的規則是以從右向左方式匹配節點的。
樣做是為了減少無效匹配次數,從而匹配快、性能更優。所以,我們在書寫 CSS Selector時,從右向左的 Selector Term 匹配節點越少越好。
不同 CSS 解析器對 CSS Rules 解析速度差異也很大,感興趣的童鞋可以看看: CSS 解析引擎,這里不再贅述。
六、高效的 ComputedStyle
瀏覽器還有一個非常棒的策略,在特定情況下,瀏覽器會共享 Computed Style ,網頁中能共享的標簽非常多,所以能極大的提升執行效率!
如果能共享,那就不需要執行匹配算法了,執行效率自然非常高。
如果兩個或多個 Element 的 ComputedStyle 不通過計算可以確認他們相等,那么這些 ComputedStyle 相等的 Elements 只會計算一次樣式,其余的僅僅共享該 ComputedStyle 。
<section class="one"> <p class="desc">One</p> </section> <section class="one"> <p class="desc">two</p> </section>
如何高效共享 Computed Style ?
1. TagName 和 Class 屬性必須一樣;
2.不能有 Style 屬性。哪怕 Style 屬性相等,他們也不共享;
3.不能使用 Sibling selector ,譬如: first-child , :last-selector , + selector4. 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 樣式就立馬開始解析,而是根據 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 。能用代碼實現的,千萬不要去浪費人力。
電腦刺綉綉花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com
八、優化策略
我們從瀏覽器構成,聊到了渲染引擎,再到 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[]{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.**,可以省略小數點之前的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
- 盡可能共享
更多請查看上文:
11.減少昂貴屬性:
當頁面發生重繪時,它們會降低瀏覽器的渲染性能。所以在編寫 CSS 時,我們應該盡量減少使用昂貴屬性,如:
- box-shadow
- border-radius
- filter
- :nth-child
12.依賴繼承。如果某些屬性可以繼承,那么自然沒有必要在寫一遍。
13.遵守 CSS 順序規則
上面就是對的一個總結,你了解 CSS 具體的實現原理,曉得規避錯誤書寫方式,知道為什么這么優化,這就夠了。
性能優化,進無止境。