深刻理解瀏覽器解析和執行過程

在咱們公司的業務場景中,有很大一部分用戶是使用老款安卓機瀏覽頁面,這些老款安卓機性能較差,若是優化不足,頁面的卡頓現象會更加明顯,此時頁面性能優化的重要性就凸顯出現。優化頁面的性能,須要對瀏覽器的渲染過程有深刻的瞭解,針對瀏覽器的每一步環節進行優化。css

頁面高性能的判斷標準是 60fps。這是由於目前大多數設備的屏幕刷新率爲 60 次/秒,也就是 60fps , 若是刷新率下降,也就是說出現了掉幀, 對於用戶來講,就是出現了卡頓的現象。html

這就要求,頁面每一幀的渲染時間僅爲16毫秒 (1 秒/ 60 = 16.66 毫秒)。但實際上,瀏覽器有其餘工做要作,所以這一幀全部工做須要在 10毫秒內完成。若是工做沒有完成,幀率將降低,而且內容會在屏幕上抖動。 此現象一般稱爲卡頓,會對用戶體驗產生負面影響。前端

瀏覽器渲染流程

瀏覽器一開始會從網絡層獲取請求文檔的內容,請求過來的數據是 Bytes,而後瀏覽器將其編譯成HTML的代碼。vue

可是咱們寫出來的HTML代碼瀏覽器是看不懂的,因此須要進行解析。html5

渲染引擎解析 HTML 文檔,將各個dom標籤逐個轉化成「DOM tree」上的 DOM 節點。同時也會解析內部和外部的css, 解析爲CSSOM tree, css treedom tree結合在一塊兒生成了render treejava

render tree構建好以後,渲染引擎隨後會經歷一個layout的階段: 計算出每個節點應該出如今屏幕上的確切座標。node

以後的階段被稱爲paiting階段,渲染引擎會遍歷render tree, 而後由用戶界面後端層將每個節點繪製出來。git

最後一個階段是 composite 階段,這個階段是合併圖層。程序員

webkit 渲染引擎的主流程

瀏覽器內核

瀏覽器是一個極其複雜龐大的軟件。常見的瀏覽器有chrome, firefox。firefox是徹底開源,Chrome不開源,但Chromium項目是部分開源。web

Chromium和Chrome之間的關係相似於嚐鮮版和正式版的關係,Chromium會有不少新的不穩定的特性,待成熟穩定後會應用到Chrome。

瀏覽器功能有不少,包括網絡、資源管理、網頁瀏覽、多頁面管理、插件和擴展、書籤管理、歷史記錄管理、設置管理、下載管理、帳戶和同步、安全機制、隱私管理、外觀主題、開發者工具等。

所以瀏覽器內部被劃分爲不一樣的模塊。其中和頁面渲染相關的,是下圖中虛線框的部分渲染引擎。

image-20180712105822858

渲染引擎的做用是將頁面轉變成可視化的圖像結果。

目前,主流的渲染引擎包括TridentGeckoWebKit,它們分別是IE、火狐和Chrome的內核(2013年,Google宣佈了Blink內核,它實際上是從WebKit複製出去的),其中佔有率最高的是 WebKit。

WebKit

最先,蘋果公司和KDE開源社區產生了分歧,複製出一個開源的項目,就是WebKit。

WebKit被不少瀏覽器採用做爲內核,其中就包括goole的chrome。

後來google公司又和蘋果公司產生了分歧,google從webkit中複製出一個blink項目。

所以,blink內核和webkit內核沒有特別的不一樣,所以不少老外會借用 chromium的實現來理解webkit的技術內幕,也是徹底能夠的。

瀏覽器源碼

瀏覽器的代碼很是的龐大,曾經有人嘗試閱讀Chromium項目的源碼,git clone 到本地發現有10個G,光編譯時間就3個小時(聽說火狐瀏覽器編譯須要更多的時間,大約爲6個小時)。所以關於瀏覽器內部到底是如何運做的,大部分的分享是瀏覽器廠商參與研發的內部員工。

國外有個很是有毅力的工程師Tali Garsiel 花費了n年的時間探究了瀏覽器的內幕,本文關於瀏覽器內部工做原理的介紹,主要整理自她的博客how browser work , 和其餘人的一些分享。

國內關於瀏覽器技術內幕主要有《WebKit技術內幕》

下面,咱們將針對瀏覽器渲染的環節,深刻理解瀏覽器內核作了哪些事情,逐一的介紹如何去進行前端頁面的優化。

瀏覽器渲染第一步:解析

解析是瀏覽器渲染引擎中第一個環節。咱們先大體瞭解一下解析究竟是怎麼一回事。

什麼是解析

通俗來說,解析文檔是指將文檔轉化成爲有意義的結構,好讓代碼去使用他們

以上圖爲例,右邊就是解析好的樹狀結構,這個結構就能夠「喂「給其餘的程序, 而後其餘的程序就能夠利用這個結構,生成一些計算的結果。

解析的過程能夠分紅兩個子過程:lexical analysis(詞法分析)syntax analysis(句法分析)

lexical analysis(詞法分析)

lexical analysis 被稱爲詞法分析的過程,有的文章也稱爲 tokenization,其實就是把輸入的內容分爲不一樣的tokens(標記),tokens是最小的組成部分,tokens就像是人類語言中的一堆詞彙。好比說,咱們對一句英文進行lexical analysis——「The quick brown fox jumps」,咱們能夠拿到如下的token:

  • 「The」
  • 「quick」
  • 「brown」
  • 「fox」
  • 「jumps」

用來作lexical analysis的工具,被稱爲**lexer**, 它負責把輸入的內容拆分爲不一樣的tokens。不一樣的瀏覽器內核會選擇不一樣的lexer , 好比說webkit 是使用Flex (Fast Lexer)做爲lexer。

syntax analysis(句法分析)

syntax analysis是應用語言句法中的規則, 簡單來講,就是判斷一串tokens組成的句子是否是正確的。

若是我說:「我吃飯工做完了」, 這句話是不符合syntax analysis的,雖然裏面的每個token都是正確的,可是不符合語法規範。須要注意的是,符合語法正確 的句子不必定是符合語義正確的。好比說,「一個綠色的夢想沉沉的睡去了」,從語法的角度來說,形容詞 + 主語 + 副詞 + 動詞沒有問題,可是語義上倒是什麼鬼。

負責syntax analysis工做的是**parser**,解析是一個不斷往返的過程。

以下圖所示,parserlexer要一個新的tokenlexer會返回一個token, parser拿到token以後,會嘗試將這個token與某條語法規則進行匹配。

若是該token匹配上了語法規則,parser會將一個對應的節點添加到 parse tree (解析樹,若是是html就是dom tree,若是是css就是 cssom tree)中,而後繼續問parser要下一個node。

固然,也有可能該tokens沒有匹配上語法規則,parser會將tokens暫時保存,而後繼續問lexertokens, 直至找到可與全部內部存儲的標記匹配的規則。若是找不到任何匹配規則,parser就會引起一個異常。這意味着文檔無效,包含語法錯誤。

syntax analysis 的輸出結果是parse tree, parse tree 的結構表示了句法結構。好比說咱們輸入"John hit the ball"做爲一句話,那麼 syntax analysis 的結果就是:

一旦咱們拿到了parse tree, 還有最後一步工做沒有作,那就是:translation,還有一些博客將這個過程成爲 compilation / transpilation / interpretation

Lexicons 和 Syntaxes

上面提到了lexerparser 這兩個用於解析工具,咱們一般不會本身寫,而是用現有的工具去生成。咱們須要提供一個語言的 lexiconsyntaxes ,才能夠生成相應的 lexerparser

webkit 使用的 lexer 和 parser 是 FlexBison

  1. flexcssflex 佈局沒有關係,是 fast-lexer 的簡寫,用來生成 lexer。 它須要一個lexicon,這個lexicon 是用一堆正則表達式來定義的 。
  2. bison 用來生成parsers, 它須要一個符合BNF範式的syntax。

lexicons

lexicons 是經過正則表達式被定義的,好比說,js中的保留字,就是lexicons 的一部分。

下面就是js中的保留字的正則表達式 的一部分。

/^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)*$/
複製代碼

syntaxes

syntaxes 一般是被一個叫無上下文語法所定義,關於無上下文語法能夠點擊這個連接,反正只須要知道,無上下文語法要比常規的語法更復雜就行了。

BNF範式

非科班出身的前端可能不瞭解 BNF 範式(說的就是我 --),它是一種形式化符號來描述給定語言的語法。

它的內容大體爲:

  1. 在雙引號中的字("word")表明着這些字符自己。
  2. 而double_quote用來表明雙引號。
  3. 在雙引號外的字(有可能有下劃線)表明着語法部分。
  4. 尖括號( < > )內包含的爲必選項。
  5. 方括號( [ ] )內包含的爲可選項。
  6. 大括號( { } )內包含的爲可重複0至無數次的項。
  7. 豎線( | )表示在其左右兩邊任選一項,至關於"OR"的意思。
  8. ::= 是「被定義爲」的意思。

下面是用BNF來定義的Java語言中的For語句的實例。

FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ]
")" statement
複製代碼

BNF 的誕生仍是挺有意思的一件事情, 有了BNF纔有了真正意義上的計算機語言。巴科斯範式直到今天,仍然是個迷,巴科斯是如何想到的

小結

咱們如今對解析過程有了一個大體的瞭解,總結成一張圖就是這樣:

對解析(parse)有了初步的瞭解以後,咱們看一下HTML的解析過程。

解析HTML

HTML是不規範的,咱們在寫html的代碼時候,好比說漏了一個閉合標籤,瀏覽器也能夠正常渲染沒有問題的。這是一把雙刃劍,咱們能夠很容易的編寫html, 可是卻給html的解析帶來很多的麻煩,更詳細的信息能夠點擊:連接

HTML lexicon

Html 的 lexicon 主要包括6個部分:

  • doctype
  • start tag
  • end tag
  • comment
  • character
  • End-of-file

當一個html文檔被lexer 處理的時候,lexer 從文檔中一個字符一個字符的讀出來,而且使用 finite-state machine 來判斷一個完整的token是否已經被完整的收到了。

HTML syntax

這裏就是html 解析的複雜所在了。html 標籤的容錯性很高,須要上下文敏感的語法。

好比說對於下面兩段代碼:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>Valid HTML</title>
  </head>
  <body>
    <p>This is a paragraph. <span>This is a span.</span></p>
    <div>This is a div.</div>
  </body>
</html>
複製代碼
<html lAnG = EN-US>
<p>This is a paragraph. <span>This is a span. <div>This is a div.
複製代碼

第一段是規範的html代碼,第二段代碼有很是多的錯誤,可是這兩段代碼在瀏覽器中都是大體相同的結構:

上面兩處代碼渲染出來的惟一的不一樣就是,正確的html會在頭部有<!DOCTYPE html>, 這行代碼會觸發瀏覽器的標準模式。

因此你看,html 的容錯性是很是高的,這樣是有代價的,這增長了解析的困難,讓詞法解析解析更加困難。

DOM Tree

HTML 解析出來的產物,通過加工,就獲得了DOM Tree。

對於下面這種html的結構:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  </head>
  <body>
    <p>
      This is text in a paragraph.
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Rubber_Duck_%288374802487%29.jpg/220px-Rubber_Duck_%288374802487%29.jpg">
    </p>
    <div>
      This is text in a div.
    </div>
  </body>
</html>
複製代碼

上面的html 的結構解析出來應該是:

說完了html的解析,咱們就該說CSS的解析了。

解析CSS

和html 解析相比,css 的解析就簡單不少了。

CSS lexicon

關於css的 lexicon, the W3C’s CSS2 Level 2 specification 中已經給出了。

CSS 中的 token 被列在了下面,下面的定義是採用了Lex風格的正則表達式。

IDENT	{ident}
ATKEYWORD	@{ident}
STRING	{string}
BAD_STRING	{badstring}
BAD_URI	{baduri}
BAD_COMMENT	{badcomment}
HASH	#{name}
NUMBER	{num}
PERCENTAGE	{num}%
DIMENSION	{num}{ident}
URI	url\({w}{string}{w}\)
|url\({w}([!#$%&*-\[\]-~]|{nonascii}|{escape})*{w}\)
UNICODE-RANGE	u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?
CDO	<!--
CDC	-->
:	:
;	;
{	\{
}	\}
(	\(
)	\)
[	\[
]	\]
S	[ \t\r\n\f]+
COMMENT	\/\*[^*]*\*+([^/*][^*]*\*+)*\/
FUNCTION	{ident}\(
INCLUDES	~=
DASHMATCH	|=
DELIM	any other character not matched by the above rules, and neither a single nor a double quote
複製代碼

花括號裏面的宏被定義成以下:

ident	[-]?{nmstart}{nmchar}*
name	{nmchar}+
nmstart	[_a-z]|{nonascii}|{escape}
nonascii	[^\0-\237]
unicode	\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
escape	{unicode}|\\[^\n\r\f0-9a-f]
nmchar	[_a-z0-9-]|{nonascii}|{escape}
num	[0-9]+|[0-9]*\.[0-9]+
string	{string1}|{string2}
string1	\"([^\n\r\f\\"]|\\{nl}|{escape})*\" string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\' badstring {badstring1}|{badstring2} badstring1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
badstring2	\'([^\n\r\f\\']|\\{nl}|{escape})*\\?
badcomment	{badcomment1}|{badcomment2}
badcomment1	\/\*[^*]*\*+([^/*][^*]*\*+)*
badcomment2	\/\*[^*]*(\*+[^/*][^*]*)*
baduri	{baduri1}|{baduri2}|{baduri3}
baduri1	url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
baduri2	url\({w}{string}{w}
baduri3	url\({w}{badstring}
nl	\n|\r\n|\r|\f
w	[ \t\r\n\f]*
複製代碼

CSS Syntax

下面是css的 syntax 定義:

stylesheet  : [ CDO | CDC | S | statement ]*;
statement   : ruleset | at-rule;
at-rule     : ATKEYWORD S* any* [ block | ';' S* ];
block       : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
ruleset     : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
selector    : any+;
declaration : property S* ':' S* value;
property    : IDENT;
value       : [ any | block | ATKEYWORD S* ]+;
any         : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
              | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
              | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
              | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
              ] S*;
unused      : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
複製代碼

CSSOM Tree

CSS解析獲得的parse tree 通過加工以後,就獲得了CSSOM TreeCSSOM 被稱爲「css 對象模型」。

CSSOM Tree 對外定義接口,能夠經過js去獲取和修改其中的內容。開發者能夠經過document.styleSheets的接口獲取到當前頁面中全部的css樣式表。

CSSOM

那麼CSSOM 到底長什麼樣子呢,咱們下面來看一下:

<!doctype html>
<html lang="en">
<head>
    <style>
        .test1 {
            color: red;
        }
    </style>
    <style>
        .test2 {
            color: green;
        }
    </style>
    <link rel="stylesheet" href="./test3.css">
</head>
<body>
    <div class="test1">TEST CSSOM1</div>
    <div class="test2">TEST CSSOM2</div>
    <div class="test3">TEST CSSOM3</div>
</body>
</html>
複製代碼

上面的代碼在瀏覽器中打開,而後在控制檯裏面輸入document.styleSheets,就能夠打印出來CSSOM,以下圖所示:

image-20180712113259360

能夠看到,CSSOM是一個對象,其中有三個屬性,均是 CSSStylelSheet 對象。CSSStylelSheet 對象用於表示每一個樣式表。因爲咱們在document裏面引入了一個外部樣式表,和兩個內聯樣式表,因此CSSOM對象中包含了3個CSSStylelSheet對象。

CSSStyleSheet

CSSStylelSheet對象又長什麼樣子呢?以下圖所示:

image-20180712113334918

CSSStyleSheet 對象主要包括下面的屬性:

  • type

    字符串 「text/css」

  • href

    表示該樣式表的來源,若是是內聯樣式,則href值爲null

  • parentStyleSheet

    父節點的styleSheet

  • ownerNode

    該樣式表所匹配到的DOM節點,若是沒有則爲空

  • ownerRule

    父親節點的styleSheet中的樣式對本節點的合併

  • media

    該樣式表中相關聯的 MediaList

  • title

    style 標籤的title屬性,不常見

    <style title="papaya whip"> body { background: #ffefd5; } </style>
    複製代碼
  • disabled

    是否禁用該樣式表,可經過js控制該屬性,從而控制頁面是否應用該樣式表

樣式表的解析

瀏覽器的渲染引擎是從上往下進行解析的。

當渲染引擎遇到 <style> 節點的時候,會立馬暫停解析html, 轉而解析CSS規則,一旦CSS規則解析完成,渲染引擎會繼續解析html

當渲染引擎遇到 <link>節點的時候,瀏覽器的網絡組件會發起對 style 文件的請求,同時渲染引擎不會暫停,而是繼續往下解析。等到 style 文件從服務器傳輸到瀏覽器的時候,渲染引擎立馬暫停解析html, 轉而解析CSS規則,一旦CSS規則解析完成,渲染引擎會繼續解析html

能夠聯想一下script的解析。

當渲染引擎遇到 <script> 節點的時候,會立馬暫停解析html

若是這個 <script> 節點是內聯,則 JS 引擎會立刻執行js代碼,同時渲染引擎也暫停了工做。何時等 JS 代碼執行完了,何時渲染引擎從新繼續工做。若是JS 代碼執行不完,那渲染引擎就繼續等着吧。

若是這個 <script> 節點是外鏈的,瀏覽器的網絡組件會發起對 script 文件的請求,渲染引擎也暫停了執行。何時等 JS 代碼下載完畢,而且執行完了,何時渲染引擎從新繼續工做。

在2011年的時候,瀏覽器廠商推出了「推測性」解析的概念。

「推測性」解析就是,當讓渲染引擎乾等着js代碼下載和運行的時候,會起一個第三個進程,繼續解析剩下的html。當js代碼下載好了,準備開始執行js代碼的時候,第三個進程就會立刻發起對剩下html所引用的資源——圖片,樣式表和js代碼的請求。

這樣就節省了以後加載和解析時間。

被稱爲「推測性」解析是由於,前面的js代碼存在必定的機率修改DOM節點,有可能會讓後面的DOM節點消逝,那麼咱們的工做就白費了。瀏覽器「推測」這樣的發生的機率比較小。

讓渲染引擎乾等着不工做是很是低效率的,因此雅虎軍規會讓把 script 標籤放在body的底部。

言歸正傳,樣式表放在head的前邊,有兩個緣由:

  1. 儘快加載樣式表
  2. 不要耽誤js代碼選擇dom節點

Render Tree

當瀏覽器忙着構建DOM TreeCSSOM Tree的時候, 瀏覽器同時將二者結合生成Render Tree。也就是說,瀏覽器構建DOM TreeCSSOM Tree ,和結合生成Render Tree,這兩個是同時進行的。

Render Forest

Levi Weintraub(webkit 的做者之一)在一次分享(分享的視頻點這裏分享的ppt點這裏)中開玩笑說,準確的來講,咱們你們提的Render Tree應該是Render Forest (森林)。由於事實上,存在多條Tree:

  • render object tree ( 稍後會詳細講解)
  • layer tree
  • inline box tree
  • style tee

這裏作一點說明。

有不少其餘的文章中提到了 Render Tree,其中的每個構成的節點都是 Render Obejcts, 所以其餘文章中的 Render Tree 概念,在本文中等同於 Render Object Tree ( Levi Weintraub 和 Webkit core 的叫法都是Render Object Tree, 其餘文章中 Render Tree的本義也應是 Render Object Tree)。

Render Object Tree 與 Dom Tree

DOM TreeRender Object Tree 之間的關係是什麼樣的?

Render Object Tree 並不嚴格等於Dom Tree,先看一張DOM Tree 和 Render Object Tree的直觀的對比圖:

image-20180711153615199

上面左側DOM tree的節點對應右側Render Object Tree上的節點。細心的你會注意到,上圖左側的DOM Tree中的HTMLDivElement 會變成RenderBlock, HTMLSpanElement 會變成RenderInline,也就是說,DOM節點對應的 render object 節點並不同。

DOM節點對應的 render object 節點並不同分這幾種狀況:

  1. display : none 的DOM 節點沒有對應的 Render Object Tree 的節點

    這裏的display:none 屬性,有多是咱們在CSS裏面設置的,也有多是瀏覽器默認的添加的屬性。好比說下面的元素就會有默認的display:none的屬性。

  • <head>
  • <meta>
  • <link>
  • <script>
  1. 一個DOM節點,可能有多個 Render Object Tree的節點

下面的各個DOM元素,會對應多個Render Object Tree的節點

  • <input type="**color**">

  • <input type="**date**">

  • <input type="**file**">

  • <input type="**range**">

  • <input type="**radio**">

  • <input type="**checkbox**">

  • <select>

    好比說,<input type="range"> 就會有兩個renderer:

  1. 脫離了文檔流的DOM節點,DOM Tree 和 Render Object Tree 是對應不上的。

脫離文檔流的狀況,要麼是float, 要麼是position: absolute / fixed

好比說對於下面的結構:

<body>	
  <div>
    <p>Lorem ipsum</p>
  </div>
</body>
複製代碼

​ 它的 DOM treeRender Tree 以下圖所示:

​ 若是增長脫離文檔流的樣式,以下:

p {
  position: absolute;
}
複製代碼

​ 狀況就會變成下面這樣:

<p> 節點對應的 Render Tree 的節點,從父節點脫離出來,掛到了頂部的RenderView 節點下面。

爲何脫離了文檔流的節點,在 Render Object Tree中的結構不一樣?脫離了文檔流的節點在構建Render Object Tree又是如何處理的?會在下面的內容中介紹。

Render Object Tree 上的節點

render object tree 是由 render object 節點構成的。render object 節點在不一樣的瀏覽器叫法不一樣,在webkit中被稱爲 renderer, 或者 被稱爲 render objects, 在firfox中,被稱爲frames

render object 的節點的類是 RenderObject,定義在源碼的目錄webkit/Source/WebCore/rendering/RenderObject.h中。

下面是RenderObject.h的簡化版本:

// Credit to Tali Garsiel for this simplified version of WebCore's RenderObject.h
class RenderObject {
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  // 這個render tree的節點所指向的那個Dom節點
  RenderStyle* style;  // 這個render tree節點的計算出來的樣式
  RenderLayer* containingLayer;  // 包含這個 render tree 的 z-index layer
}
複製代碼

RenderBox 是RenderObject的一個子類,它主要是負責DOM樹上的每個節點的盒模型。

RenderBox 包括一些計算好尺寸的信息,好比說:

  • height
  • width
  • padding
  • border
  • margin
  • clientLeft
  • clientTop
  • clientWidth
  • clientHeight
  • offsetLeft
  • offsetTop
  • offsetWidth
  • offsetHeight
  • scrollLeft
  • scrollTop
  • scrollWidth
  • scrollHeight

render object 的節點的做用以下:

  • 負責 layout 和 paint

  • 負責查詢DOM元素查詢尺寸API

    好比說獲取offsetHeight, offsetWidth的屬性

render object 節點的類型

咱們在CSS中接觸過文檔流的概念,文檔流中的元素分爲塊狀元素和行內元素,好比說div是塊狀元素,span是行內元素。塊狀元素和行內元素在文檔流中的表現不一樣,就是在這裏決定的。

Render Object 的節點類型分爲下面幾種:

  1. RenderBlock

    display: block 的DOM節點對應的render object節點類型爲RenderBlock

  2. RenderInline

    display:inline 的DOM節點對應的render object節點類型爲RenderInline

  3. RenderReplaced

    可能咱們以前據說過「替換元素」 的概念,好比說常見的「替換元素」有下面:

    • <iframe>
    • <video>
    • <embed>
    • <img>

    爲啥被稱爲「替換元素」,是由於他們的內容會被一個獨立於HTML/CSS上下文的外部資源所替代。

    「替代元素」 的DOM節點對應的render object 節點類型爲RenderReplaced

  4. RenderTable

    <table> 元素的DOM節點對應的render object 節點類型爲 RenderTable

  5. RenderText

    文本內容的DOM節點對應的render object 的節點類型爲 RenderText

源碼大概長這個樣子:

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}
複製代碼

上面5中類型的Render Object 的節點之間的關係組合並非沒有準則的,在咱們寫出嵌套不規範的HTML時,渲染引擎幫咱們作了不少事情。

Anonymous renderers

render object tree 遵照2個準則:

  • 在文檔流中的塊狀元素的子節點,要麼都是塊狀元素,要麼都是行內元素。
  • 在文檔流中的行內元素的子節點,只能都是行內元素。

anonymounse renderers(匿名的render object 節點)就是用於處理不遵照這兩種規則的代碼的,若是出現不符合這兩個準則的狀況,好比說下面:

  1. 若在塊狀元素裏面同時出現了塊狀元素和行內元素:
<div>
  Some text
  <div>
    Some more text
  </div>
</div>
複製代碼

上面的代碼中,最外層的div節點有兩個子節點,第一個子節點是行內元素,第二個子節點是塊狀元素。render object tree 中會構建一個anonymounse renderer去包裹 text 節點,所以上面的代碼變成了下面的:

<div>
  <anonymous block>
    Some text
  </anonymous block>
  <div>
   Some more text
  </div>
</div>
複製代碼
  1. 還有另一種很是糟糕的狀況,就是在行內元素中出現了塊狀元素:
<i>Italic only <b>italic and bold
  <div>
    Wow, a block!
  </div>
  <div>
    Wow, another block!
  </div>
More italic and bold text</b> More italic text</i>

複製代碼

上面的代碼中,render object tree須要作更多的事情去修復這種糟糕的DOM tree: 三個anonymounse renderers會被建立,上面的代碼會被分割成三段,被三個匿名的block 包裹。

<anonymous pre block>
  <i>Italic only <b>italic and bold</b></i>
</anonymous pre block>

<anonymous middle block>
  <div>
  Wow, a block!
  </div>
  <div>
  Wow, another block!
  </div>
</anonymous middle block>

<anonymous post block>
  <i><b>More italic and bold text</b> More italic text</i>
</anonymous post block>

複製代碼

注意到,<i> 元素和 <b> 元素都被分割進了<anonymous pre block><anonymous post block> 兩個類型爲 RenderBlock 的節點中,他們經過一種叫*continuation chain(延續鏈)*的機制來連接。

負責上面遞歸拆分行內元素的生產*continuation chain(延續鏈)*的方法被稱爲 splitFlow

所以,一旦你寫出了不符合規範的html結構, 在構建render object tree時就須要更多的工做去糾正,從而形成頁面性能的降低。

構建 Render Object Tree

GeckoWebKit 採用了不一樣的方案來構建 Render Tree

Gecko 是把樣式計算和構建Render Object Tree 的工做代理到 FrameConstructor 對象上。而 webkit 採用的方案是,每個DOM節點本身計算本身的樣式,而且構建本身 的Render object tree 對應的節點。

Gecko 針對DOM的更新增長了一個 listener,當DOM 更新的時候,更新的DOM節點被傳到一個指定的對象FrameConstructor, 這個FrameConstructor 會爲 DOM 節點計算樣式,同時爲這個DOM節點建立一個合適的 Render Object Tree節點

WebKit構建 Render Object tree 的過程被稱爲attachment, 每個DOM節點被賦予一個 attach() 方法,這是一個同步的方法,當每個DOM節點被插入DOM樹的時候, 該DOM節點的attach()方法就會被調用。

樣式計算

在構建Render Object Tree的時候,須要進行樣式計算,也就是Render Tree每個節點都須要有一個visual information的信息,才能夠被繪製在屏幕上,這就須要樣式計算這一過程。

而樣式計算須要兩部分「原材料」:

  1. DOM Tree
  2. 一堆樣式規則

DOM Tree在HTML解析以後就能夠拿到了,一堆樣式規則能夠來自下面:

  • 瀏覽器默認的樣式
  • 外鏈樣式
  • 內聯樣式
  • DOM節點上的style屬性

那麼樣式規則是如何構成的呢?

  • 樣式表是一堆 規則(rules)的集合;
  • 固然也不光都是 規則(rules), 還會有一些奇怪的東西:@import, @media, @namespace 等等
  • 一個**規則(rules)是由選擇器(selector)聲明塊(declaration block)**構成的
  • **聲明塊(declaration block)由一堆聲明(declaration)**加中括號構成
  • **聲明(declaration ** 由 property 和 value 構成。

樣式計算存在如下三個難點:

  1. style 樣式數據太多,會佔用大量內存
  2. 匹配元素會影響性能
  3. css規則的應用順序

下面咱們介紹這個三個難點是如何解決的。

樣式規則的應用順序

某一個DOM節點上可能有多個規則,好比下下面:

div p {
  color: goldenrod;
}
p {
  color: red;
}

複製代碼

那麼這個DOM節點究竟用的是哪一個規則?

規則的權重是:先看 order , 而後再算specificity, 最後再看哪一個規則靠的更近。

order

order的權重從高到底:

  1. 用戶的 ! important 聲明(瀏覽器可讓用戶導入自定的樣式)
  2. 程序員寫的 ! important 聲明
  3. 程序員寫的普通css樣式
  4. 瀏覽器的默認css樣
Specificity

Specificity是一個相加起來的值

#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */

複製代碼
  1. 第一位的數值(a)

    是否有DOM節點上style屬性的值,有則是1,不然是0

  2. 第二位的數值 (b)

    id選擇器的數量之和

  3. 第三位的數值 (c)

    class選擇器,屬性選擇器,僞類選擇器個數之和

  4. 第四位的數值 (d)

    標籤選擇器,僞元素選擇器個數之和

下面是例子:

*             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

複製代碼

style數據太多,佔用大量內存

這裏的style數據太多,不是說咱們寫的css樣式太多,而是Render Object Tree每個節點上都須要存儲所有的CSS樣式,那些沒有被指定的樣式,其值爲繼承父節點的樣式,或者是瀏覽器的默認樣式,或者乾脆是個空值。

webkit 和 gecko 採用了不一樣的解決方案。

webkit:共享樣式數據

WebKit 的解決方案是,節點們會引用RenderStyle對象。這些對象在如下狀況下能夠由不一樣的DOM節點共享,從而節省空間和提升性能。

  • 這些節點是同級關係
  • 這些節點有相同的僞類狀態:hover、:active、:focus
  • 這些節點都沒有id
  • 這些節點有相同的tag名稱
  • 這些節點有相同的class
  • 這些節點都沒有經過style屬性設置的樣式
  • 這些節點沒有一個是使用 兄弟選擇器的,好比說: div + pdiv ~ p:last-child:first-child:nth-child():nth-of-type()
Gecko:style struct sharing

Gecko 採用了一種 style struct sharing 的機制。有一些css屬性能夠聚合在一塊兒,好比說font-size, font-family, color 等等,瀏覽器就把這些能夠被劃分爲一組的屬性,單獨的保存到一個對象裏面,這個對象被稱爲 style struct。以下表所示:

image-20180705203212950

上圖中的,computed style 裏就不用存儲CSS所有的200多個屬性,而是保存着對這些 style struct對象的引用。

這樣一來,一些具備相同屬性的DOM 節點就能夠引用相同的 style struct, 不只如此,由於子節點有一些屬性會繼承父節點,那麼保存這些屬性的 style struct 就會被父節點和子節點所共享。

匹配元素會影響性能

對於每個DOM節點,css引擎須要去遍歷全部的css規則看是否匹配。對於大部分的DOM節點,css規則的匹配並不會發生改動。

好比說,用戶把鼠標hover到一個父元素上面,這個父元素的css規則匹配是發生了變化,咱們不只僅要從新計算這個父元素的樣式,還須要從新計算這個父元素的子元素的樣式(由於要處理繼承的樣式),可是能匹配這些子元素的樣式規則,是不會變的。

若是咱們能記下來,有哪些selector能夠匹配到就行了。

爲了優化這一點,CSS 引擎在進行 selector 匹配時,會根據權重的順序把他們排成一串,而後把這一串加到右邊的 CSS rule tree 上面。

image-20180706013726314

CSS引擎但願右邊CSS rule tree 的分支數越少越好,所以會將新加入的一串儘可能的合併到已有的分支,因此上面的過程會是下面這樣的:

image-20180706013959120

而後遍歷每個DOM節點去找能匹配到的CSS Rule Tree的分支,從CSS rule Tree的底部開始,一路向上開始匹配,直到找到對應的那一條 style rule Tree分支。這就是人們口中常說的,css選擇器是從右邊匹配的。

當瀏覽器由於某種緣由(用戶交互,js修改DOM)進行從新渲染的時候,CSS引擎會快速檢查一下,對父節點的改動是否會影響到子節點的 selector 匹配,若是不影響,CSS引擎就直接拿到每個子節點對CSS rule Tree 對應那個分支的指針,節省掉匹配選擇器的時間。

儘管如此,咱們仍是須要在第一次遍歷每個DOM節點去找到對應的CSS Rule Tree的分支。若是咱們有10000個相同的節點,就須要遍歷10000次。

Gecko 和 Webkit 都對此進行了優化,在遍歷完一個節點以後,會把計算好的樣式放到緩存中,在遍歷下一個節點以前,會作一個判斷,看是否能夠複用緩存中的樣式。

這個判斷包括一下幾點:

  1. 兩個節點是否有相同的id, class

  2. 兩個節點是否有相同的style 屬性

  3. 兩個節點對應的父親節點是否共享一份計算好的樣式,那該兩個節點繼承的樣式也是相同的。

    image-20180706021216963

解析階段如何優化

更加符合規範的html結構

上面在構建render object tree 的過程當中,會額外作不少工做處理咱們不符合規範的DOM 結構,好比說,調用splitflow 方法分割代碼,用 anonymous renderBlock 包裹不符合規範的節點。

以前咱們都聽過建議:「要編寫更有語義,更符合規範的html結構「,緣由就在於此,可讓渲染引擎作更少的事情。

下面是模擬一種不不符合規範的狀況:

<i v-for="n in 1000">
        Italic only
        <b>italic and bold
          <div>
            Wow, a block!
          </div>
          <div>
            Wow, another block!
            <b>More italic and bold text</b>
            <div>
              More italic and bold text
              <p>More italic and bold text</p>
            </div>
          </div>
          More italic and bold text
          More italic and bold text
        </b> More italic text
      </i>

複製代碼

在控制檯裏面,設置cpu 爲6x slowdown,而後記錄渲染數據以下:

image-20180712122251219

其中花費了 12888ms 進行了rendering 過程。

若是咱們對html代碼僅僅作幾處修改,在不考慮css優化、樣式優化的前提下:

<div v-for="n in nums">
        <p>Italic only</p>
        <div>italic and bold
          <div>
            Wow, a block!
          </div>
          <div>
            Wow, another block!
            <b>More italic and bold text</b>
            <div>
              More italic and bold text
              <p>More italic and bold text</p>
            </div>
          </div>
          More italic and bold text
          More italic and bold text
        </div>
      </div>

複製代碼

在控制檯裏面,設置cpu 爲6x slowdown,而後記錄渲染數據以下:

image-20180712122830930

能夠發現,render 階段的渲染時間爲11506ms,rendering 階段渲染的時間相比於12888ms減小了1382ms,時間縮短了12%

一次測量可能有偏差,但不管進行屢次測量,都會發現第二種的代碼的渲染時間要小於第一種代碼的渲染時間。

選擇器的優化

不一樣的選擇器,匹配的效率會有差距,可是差距不大。

咱們用一個有1000個DOM節點的頁面來測試,分別在5個瀏覽器中嘗試如下20種匹配器:

1. Data Attribute (unqualified)
	*/
	[data-select] {
		color: red;
	}

	/*
		2. Data Attribute (qualified)
	

	a[data-select] {
		color: red;
	}
	*/
	

	/*
		3. Data Attribute (unqualified with value)
	

	[data-select="link"] {
		color: red;
	}
	*/


	/*
		4. Data Attribute (qualified with value)
	

	a[data-select="link"] {
		color: red;
	}
	*/


	/*
		5. Multiple Data Attributes (qualified with values)
	

	div[data-div="layer1"] a[data-select="link"] {
		color: red;
	}
	*/


	/*
		6. Solo Pseudo selector
	

	a:after {
		content: "after";
		color: red;
	}
	*/


	/*
		7. Combined classes
	

	.tagA.link {
		color: red;
	}
	*/


	/*
		8. Multiple classes 
	

	.tagUl .link {
		color: red;
	}
	*/


	/*
		9. Multiple classes (using child selector)
	
	.tagB > .tagA {
		color: red;
	}
	*/


	/*
		10. Partial attribute matching

	[class^="wrap"] {
		color: red;
	}	
	*/


	/*
		11. Nth-child selector
	
	.div:nth-of-type(1) a {
		color: red;
	}
	*/


	/*
		12. Nth-child selector followed by nth-child selector
	
	.div:nth-of-type(1) .div:nth-of-type(1) a {
		color: red;
	}
	*/


	/*
		13. Insanity selection (unlucky for some)
	
	div.wrapper > div.tagDiv > div.tagDiv.layer2 > ul.tagUL > li.tagLi > b.tagB > a.TagA.link {
		color: red;
	}
	*/


	/*
		14. Slight insanity
	
	.tagLi .tagB a.TagA.link {
		color: red;
	}
	*/


	/*
		15. Universal
	
	* {
		color: red;
	}
	*/


	/*
		16. Element single
	
	a {
		color: red;
	}
	*/


	/*
		17. Element double
	
	div a {
		color: red;
	}
	*/


	/*
		18. Element treble
	
	div ul a {
		color: red;
	}
	*/


	/*
		19. Element treble pseudo
	
	div ul a:after; {
		content: "after";
		color: red;
	}
	*/


	/*
		20. Single class
	
	.link {
		color: red;
	}
複製代碼

測試的結果以下:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
1 56.8 125.4 63.6 152.6 1455.2
2 55.4 128.4 61.4 141 1404.6
3 55 125.6 61.8 152.4 1363.4
4 54.8 129 63.2 147.4 1421.2
5 55.4 124.4 63.2 147.4 1411.2
6 60.6 138 58.4 162 1500.4
7 51.2 126.6 56.8 147.8 1453.8
8 48.8 127.4 56.2 150.2 1398.8
9 48.8 127.4 55.8 154.6 1348.4
10 52.2 129.4 58 172 1420.2
11 49 127.4 56.6 148.4 1352
12 50.6 127.2 58.4 146.2 1377.6
13 64.6 129.2 72.4 152.8 1461.2
14 50.2 129.8 54.8 154.6 1381.2
15 50 126.2 56.8 154.8 1351.6
16 49.2 127.6 56 149.2 1379.2
17 50.4 132.4 55 157.6 1386
18 49.2 128.8 58.6 154.2 1380.6
19 48.6 132.4 54.8 148.4 1349.6
20 50.4 128 55 149.8 1393.8
Biggest Diff. 16 13.6 17.6 31 152
Slowest 13 6 13 10 6

解釋

在瀏覽器的引擎內部,這些選擇器會被從新的拆分,組合,優化,編譯。而不一樣的瀏覽器內核採用不一樣的方案,因此幾乎沒有辦法預測,選擇器的優化究竟能帶來多少收益。

結論:

合理的使用選擇器,好比說層級更少的class,的確會提升匹配的速度,可是速度的提升是有限的 。

若是你經過dev tool 發現匹配選擇器的確是瓶頸,那麼就選擇優化它。

精簡沒有用的樣式代碼

大量無用代碼會拖慢瀏覽器的解析速度。

用一個3000行的無用css樣式表和1500行的無用樣式表,進行測試:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
3000 64.4 237.6 74.2 436.8 1714.6
1500 51.6 142.8 65.4 358.6 1412.4

對於火狐來講,在其餘環節一致的狀況下,頁面渲染的速度幾乎提高了一倍

儘管如今的慣例是把css 打包成一個巨大單一的css文件。這樣作的確是有好處的,減小http請求的數量。可是拆分css文件可讓加載速度更快,瀏覽器的解析速度更快。

這一項的優化是很是顯著的,一般能夠省下來 2ms ~ 300ms的時間。

精簡的過程可使用uncss 工具來自動化的完成。

瀏覽器渲染第二步:layout

在上一節咱們提到了 render object treerender object 的節點第一次被建立而後添加到 render object tree時,它身上沒有關於位置和尺寸的信息。接下來,肯定每個render object的位置和尺寸的過程被稱爲layout。

咱們能在不一樣的文章中看到不一樣的名詞: 佈局layout , 迴流reflow , 這些名詞說的都是一回事,不一樣瀏覽器的叫法不一樣。

每個renderer節點 都有layout 方法。 在構建renderer節點的時候就聲明瞭這個方法:

class RenderObject {
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  // 這個render tree的節點所指向的那個Dom節點
  RenderStyle* style;  // 這個render tree節點的計算出來的樣式
  RenderLayer* containingLayer;  // 包含這個 render tree 的 z-index layer
}
複製代碼

layout ()是一個遞歸的過程。layout 過程到底是誰來負責的呢? 一個名爲 FrameView 的 class。

FrameView 能夠運行下面兩種類型的 layout :

  1. 全局layout

    render tree 的根節點自身的layout方法被調用,而後整個render tree 被更新。

  2. 局部layout

    只是區域性的更新,只適用於某個分支的改動不會影響到周圍的分支。

    目前局部layout只會在 text 更新的時候使用

Dirty Bits

在layout 階段,採用一種稱爲 Dirty Bits 的機制去判斷一個節點是否須要layout。當一個新的節點被插到tree中時,它不只僅「弄髒「了它自身,還「弄髒「了相關的父節點(the relevant ancestor chain,下面會介紹)。有沒有被「弄髒」是經過設置bits (set bits)來標識的。

bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout || m_posChildNeedsLayout; }
複製代碼

上面 needsLayout 爲 true 有三種狀況:

  1. selfNeedsLayout

    Rederer 自身是 「髒」的。當一個 rederer 自身被設置爲「髒」的,它相關的父親節點也會被設置一個標識來指出它們有一個「髒」的子節點

  2. posChildNeedsLayout

    設置了postion不爲static的子節點被弄髒了

  3. normalChildNeedsLayout

    在文檔流中的子節點被弄髒了

上面之因此要區分子節點是否在文檔流中,是爲了layout過程的優化。

Containing Block (包含塊)

上面提到了相關父節點(the relevant ancestor chain),那麼到底是如何判斷哪一個節點是 **相關父節點 **?

答案就是經過 containing block.

Container Block(包含塊) 身份有兩個

  1. 子節點的相關的父節點

  2. 子節點的相對座標系

    子節點都有 XPos 和 YPos 的座標,這些座標都是相對於他們的Containing Block (包含塊)而言的。

下面介紹Container Block(包含塊) 概念。

包含塊的定義

通俗來說,Container Block 是決定子節點位置的父節點。每一個子節點的位置都是相對於其container block來計算的。更詳細的信息能夠點這個 css2.1 官方的解釋點這裏

有一種特殊的containing block —— initial containing block (最初的container block)。

當Docuement 節點上的 renderer() 方法被調用時,會返回一個節點對象爲render tree 的根節點,被稱做 RenderView, RenderView 對應的containing bock 就是 initial containing block。

initial containing block 的尺寸永遠是viewport的尺寸,且永遠是相對於整個文檔的 position(0,0) 的位置。下面是圖示:

image-20180712172110827

黑色的框表明的是 initial containing block (最初的container block) , 灰色的框表示整個 document。當document往下滾動的時候, initial containing block (最初的container block) 就會被移出了屏幕。 initial containing block (最初的container block) 始終在document 的頂部,而且大小始終是 viewport 的尺寸。

那麼render Tree上的節點,它們各自的 containing block 是什麼?

  • 根節點的 containing block 始終是 RenderView

  • 若是一個renderer節點的css postion 的值爲 relative 或 static,則其 containing block 爲最近的父節點

  • 若是一個renderer節點的css postion 的值爲 absolute, 則其containing block 爲最近的 css postion 的值不爲static 的父節點。若是這樣的父節點不存在,則爲 RenderView,也就是根節點的containing block

  • 若是一個renderer節點的css postion 的值爲 fixed。這個狀況有一些特殊,由於 W3C 標準和 webkit core 介紹的不同。W3C 最新的標準認爲css postion 的值爲 fixed的renderer節點的containing block是viewport ,原文以下:

    image-20180712230128732

    而webkit core 認爲css postion 的值爲 fixed的renderer節點的containing block是RenderView。RenderView並不會表現的和viewport同樣,可是RenderView會根據頁面滾動的距離算出css postion 的值爲 fixed的renderer節點的位置。這是由於單獨爲viewport 生成一個renderer 節點並不簡單。原文以下:

    image-20180712230517294

render tree 有兩個方法用來判斷 renderer 的position:

bool isPositioned() const;   // absolute or fixed positioning
bool isRelPositioned() const;  // relative positioning

複製代碼

render tree 有一個方法用來獲取某一個塊狀 rederer 的containing block(相對父節點)

RenderBlock* containingBlock() const

複製代碼

render tree 還有一個方法是兼容了行內元素獲取相對父節點的方法,用來代替containingBlock (由於containingBlock只適用於塊狀元素)

RenderObject* container() const

複製代碼

當一個 renderer 被標記爲須要 layout的時候,就會經過container()找到相對父節點,把isPositioned 的狀態傳遞給相對父節點。若是 renderer 的position是absolute 或 fixed ,則相對父節點的posChildNeedsLayout爲true,若是 renderer的position 是 static 或 relative , 則相對父節點的 normalChildNeedsLayout 爲 true。

會觸發layout 的屬性

  1. 盒子模型相關的屬性

    • width

    • height

    • padding

    • margin

    • border

    • display

    • ……
  2. 定位屬性和浮動

    • top
    • bottom
    • left
    • right
    • position
    • float
    • clear
  3. 節點內部的文字結構

    • text - aligh
    • overflow
    • font-weight
    • font- family
    • font-size
    • line-height

上面只是一部分,更所有的能夠點擊 csstriggers 來查閱;

csstrigger 裏面須要注意的有幾點。

  1. opacity的改動,在blink內核和Gecko內核上不會觸發layout 和 repaint

    image-20180706152622532

  2. transform的改動,在blink內核和Gecko內核上不會觸發layout 和 repaint

    image-20180706152905325

  3. visibility 的改動,在Gecko 內核上不會觸發 layout repaint, 和 composite

    image-20180706153256502

會觸發layout 的方法

幾乎任何測量元素的寬度,高度,和位置的方法都會不可避免的觸發reflow, 包括可是不限於:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • and a lot more…

如何避免重複Layout

不要頻繁的增刪改查DOM

不要頻繁的修改默認根字體大小

不要一條條去修改DOM樣式,而是經過切換className

雖然切換className 也會形成性能上的影響,可是次數上減小了。

「離線」修改DOM

好比說必定要修改這個dom節點100次,那麼先把dom的display設置爲 none ( 僅僅會觸發一次迴流 )

使用flexbox

老的佈局模型以相對/絕對/浮動的方式將元素定位到屏幕上 Floxbox佈局模型用流式佈局的方式將元素定位到屏幕上,flex性能更好。

不要使用table

使用table佈局哪怕一個很小的改動都會形成從新佈局

避免強制性的同步layout

layout根據區域來劃分的,分爲全局性layout, 和局部的layout。好比說修改根字體的大小,會觸發全局性layout。

全局性layout是同步的,會馬上立刻被執行,而局部性的layout是異步的,分批次的。瀏覽器會嘗試合併屢次局部性的layout爲一次,而後異步的執行一次,從而提升效率。

可是js一些操做會觸發強制性的同步佈局,從而影響頁面性能,好比說讀取 offsetHeight、offsetWidth 值的時候。

瀏覽器渲染第三步:paint

第三個階段是paint 階段

會觸發paint 的屬性

  • color
  • border - style
  • border - radius
  • visibility
  • Text -decoration
  • background
  • background
  • Background - image
  • background - size
  • Background - repeat
  • background - position
  • outline - color
  • outline
  • outline - style
  • outline - width
  • box - shadow

如何優化

使用transform代替top, left 的變化

使用transform不會觸發layout , 只會觸發paint。

若是你想頁面中作一些比較炫酷的效果,相信我,transform能夠知足你的需求。

// 位置的變換
transform: translate(1px,2px)

// 大小的變換
transform: scale(1.2)

複製代碼

使用opacity 來代替 visibility

由於 visibility屬性會觸發重繪,而opacity 則不會觸發重繪

避免使用耗性能的屬性

能夠點擊這個連接進行測試測試鏈接

.link {
    background-color: red;
    border-radius: 5px;
    padding: 3px;
    box-shadow: 0 5px 5px #000;
    -webkit-transform: rotate(10deg);
    -moz-transform: rotate(10deg);
    -ms-transform: rotate(10deg);
    transform: rotate(10deg);
    display: block;
}

複製代碼

測試結果:

Test Chrome 34 Firefox 29 Opera 19 IE9 Android 4
Expensive Styles 65.2 151.4 65.2 259.2 1923

須要注意的是,高耗css樣式若是不會頻繁的觸發迴流和重繪,只會在頁面渲染的時候被執行一次,那麼對頁面的性能影響是有限的。若是頻繁的觸發迴流和重繪,那麼最基本的css樣式也會影響到頁面的性能。

那麼哪些 css 樣式會形成頁面性能的問題呢?

  • Border-radius
  • Shadow
  • gradients
  • transform rotating

更多的內容請參考 鏈接

瀏覽器渲染第四步:composite

什麼是合成層

上面幾個階段能夠用下面一張圖來表示:

1. 從 Nodes 到 LayoutObjects

DOM 樹每一個 Node 節點都有一個對應的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的內容。

2. 從 LayoutObjects 到 PaintLayers

有相同座標的 LayoutObjects ,在同一個PaintLayer內。 根據建立PaintLayer 的緣由不一樣,能夠將其分爲常見的 3 類:

  1. NormalPaintLayer
  • 根元素
  • relative、fixed、sticky、absolute
  • opacity 小於 1
  • CSS 濾鏡(fliter)
  • 有 CSS mask 屬性
  • 有 CSS mix-blend-mode 屬性(不爲 normal)
  • 有 CSS transform 屬性(不爲 none)
  • backface-visibility 屬性爲 hidden
  • 有 CSS reflection 屬性
  • 有 CSS column-count 屬性(不爲 auto)或者 有 CSS column-width 屬性(不爲 auto)
  • 當前有對於 opacity、transform、fliter、backdrop-filter 應用動畫
  1. OverflowClipPaintLayer
  • overflow 不爲 visible
  1. NoPaintLayer
  • 不須要 paint 的 PaintLayer,好比一個沒有視覺屬性(背景、顏色、陰影等)的空 div。

4. 從 PaintLayers 到 GraphicsLayers

某些特殊的paintLayer會被當成合成層,合成層擁有單獨的 GraphicsLayer,而其餘不是合成層的paintLayer,則和其第一個擁有GraphicsLayer 父層公用一個。

每一個 GraphicsLayer 都有一個GraphicsContextGraphicsContext 會負責輸出該層的位圖,位圖是存儲在共享內存中,做爲紋理上傳到 GPU 中,最後由 GPU 將多個位圖進行合成,而後 draw 到屏幕上,此時,咱們的頁面也就展示到了屏幕上。

渲染層提高爲合成層的緣由

渲染層提高爲合成層的緣由有一下幾種:

  • 直接緣由
    • 硬件加速的 iframe 元素(好比 iframe 嵌入的頁面中有合成層
    • video元素
    • 3d transiform
    • 在 DPI 較高的屏幕上,fix 定位的元素會自動地被提高到合成層中。但在 DPI 較低的設備上卻並不是如此
    • backface-visibility 爲 hidden
    • 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition(須要注意的是 active 的 animation 或者 transition,當 animation 或者 transition 效果未開始或結束後,提高合成層也會失效)
    • will-change 設置爲 opacity、transform、top、left、bottom、right(其中 top、left 等須要設置明確的定位屬性,如 relative 等)
  • 後代元素緣由
    • 有合成層後代同時自己有 transform、opactiy(小於 1)、mask、fliter、reflection 屬性
    • 有合成層後代同時自己 overflow 不爲 visible(若是自己是由於明確的定位因素產生的 SelfPaintingLayer,則須要 z-index 不爲 auto)
    • 有合成層後代同時自己 fixed 定位
    • 有 3D transfrom 的合成層後代同時自己有 preserves-3d 屬性
    • 有 3D transfrom 的合成層後代同時自己有 perspective 屬性
  • overlap 重疊緣由

爲啥overlap 重疊也會形成提高合成層渲染? 圖層之間有重疊關係,須要按照順序合併圖層。

如何優化

若是把一個頻繁修改的dom元素,抽出一個單獨的圖層,而後這個元素的layout, paint 階段都會在這個圖層進行,從而減小對其餘元素的影響。

使用will-change 或者 transform3d

使用 will-change 或者 transform3d

1. will-change: transform/opacity
 2. transform3d(0,0,0,)

複製代碼

使用加速視頻解碼的

由於視頻中的每一幀都是在動的,因此視頻的區域,瀏覽器每一幀都須要重繪。因此瀏覽器會本身優化,把這個區域的給抽出一個單獨的圖層

擁有3D(webgl) 上下文或者加速的2D上下文的 節點

混合插件(flash)

若是某一個元素,經過z-index在複合層上面渲染,則該元素也會被提高到複合層

須要注意的是,gif 圖片雖然也變化很頻繁,可是 img 標籤不會被單獨的提到一個複合層,因此咱們須要單獨的提到一個獨立獨立的圖層之類。

composite更詳盡的知識能夠了解下面這個博客: 《GPU Accelerated Compositing in Chrome》

頁面性能優化實踐

Bounce-btn優化

bounce-btn是相似於下面這種的:

若是想實現這種效果,假設不考慮性能問題,寫出下面的代碼話:

<div class="content-box"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>
    <div class="bounce-btn"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>
    <div class="content-box"></div>

複製代碼
.bounce-btn {
  width: 200px;
  height: 50px;
  background-color: antiquewhite;
  border-radius: 30px;
  margin: 10px auto;
  transition: all 1s;
}
.content-box {
  width: 400px;
  height: 200px;
  background-color: darkcyan;
  margin: 10px auto;
}

複製代碼
let btnArr = document.querySelectorAll('.bounce-btn');
setInterval(() => {
  btnArr.forEach((dom) => {
    if ( dom.style.width ==='200px') {
      dom.style.width = '300px';
      dom.style.height = '70px';
    } else {
      dom.style.width = '200px';
      dom.style.height = '50px';
    }
  })
},2000)

複製代碼

能夠發現這樣的性能是很是差的,咱們打開dev-tool的paint flashing, 發現從新渲染的區域如綠色的區域所示:

而此時的性能是,1000ms 的時間內,layout階段花費了29.9ms佔了18.6%

image-20180706144354901

image-20180706144409030

這個其實有兩個地方,第一是,bounce btn 這個元素被js 修改了width 、height 這些屬性,從而觸發了自身layout ——> repaint ——> composite。第二是,bounce btn 沒有脫離文檔流,它自身佈局的變化,影響到了它下面的元素的佈局,從而致使下面元素也觸發了layout ——> repaint ——> composite

那麼咱們把修改width, 改成 tansform: scale()

let btnArr = document.querySelectorAll('.bounce-btn');
setInterval(() => {
  btnArr.forEach((dom) => {
    if ( dom.style.transform ==='scale(0.8)') {
      dom.style.transform = 'scale(2.5)';
    } else {
      dom.style.transform = 'scale(0.8)';
    }
  })
},2000)

複製代碼

頁面性能獲得了提升:

從新渲染的區域只有它自身了。此時的性能是,1000ms 的時間內,沒有存在layout階段,

image-20180706145450446

image-20180706145652251

若是繼續優化,咱們經過aimation動畫來實現bounce的效果:

@keyframes bounce {
            0% {
                transform: scale(0.8);
            }
            25% {
                transform: scale(1.5);
            }
            50% {
                transform: scale(1.5);
            }
            75% {
                transform: scale(1.5);
            }
            100% {
                transform: scale(0.8);
            }
        }

複製代碼

頁面中沒有從新渲染的區域:

而且頁面性能幾乎沒有受到任何影響,不會從新經歷 layout ——> repaint ——> composite.

image-20180706150428460

image-20180706150438553

因此,對於這種動效,優先選擇 CSS animation > transform 修改 scale > 絕對定位 修改width > 文檔流中修改width

跑馬燈的優化

跑馬燈的動效是:每隔3秒進行向左側滑動淡出,而後再滑動從新淡入,更新文本爲「**砍價9元」

以前的滑動和淡出的效果是經過vue提供的 <transision> 來實現的

<transision> 原理

當咱們想要用到過渡效果,會在vue中寫這樣的代碼:

<transition name="toggle">
  <div class="test">
</transition>

複製代碼

可是其實渲染到瀏覽器中的代碼,會依次是下面這樣的:

// 過渡進入開始的一瞬間
<div class="test toggle-enter">

// 過渡進入的中間階段
<div class="test toggle-enter-active">

// 過渡進入的結束階段
<div class="test toggle-enter-active toggle-enter-to">


// 過渡淡出開始的一瞬間
<div class="test toggle-leave">

// 過渡淡出的中間階段
<div class="test toggle-leave-active">

// 過渡淡出的結束階段
<div class="test toggle-leave-active toggle-leave-to">

複製代碼

也就是說,過渡效果的實現,是經過不停的修改、增長、刪除該dom節點的class來實現。

<transision> 影響頁面性能

一方面, v-if 會修改dom節點的結構,修改dom節點會形成瀏覽器重走一遍 layout 階段,也就是重排。另外一方面,dom節點的class被不停的修改,也會致使瀏覽器的重排現象,所以頁面性能會比較大的受到影響。

若頁面中 <transition> 控制的節點過多時,頁面的性能就會比較受影響。

爲了證實,下面代碼模擬了一種極端的狀況:

<div v-for="n in testArr">
  <transition name="toggle">
    <div class="info-block" v-if="isShow"></div>
  </transition>
</div>

複製代碼
export default {
  	data () {
          return {
            isShow: false,
            testArr: 1000
          }
    },
    methods: {
	    toggle() {
	    	var self = this;
	    	setInterval(function () {
		      self.isShow = !self.isShow
	      }, 1000)
      }
    },
    mounted () {
	 this.toggle()
    }
  }

複製代碼
.toggle-show-enter {
    transform: translate(-400px,0);
  }

  .toggle-show-enter-active {
    color: white;
  }

  .toggle-show-enter-to {
    transform: translate(0,0);
  }

  .toggle-show-leave {
    transform: translate(0,0);
  }

  .toggle-show-leave-to {
    transform: translate(-400px,0);
  }

  .toggle-show-leave-active {
     color: white;
  }

複製代碼

上面的代碼在頁面中渲染了 1000 個過渡的元素,這些元素會在1秒的時間內從左側劃入,而後劃出。

此時,咱們打開google瀏覽器的開發者工具,而後在 performance 一欄中記錄分析性能,以下圖所示:

能夠發現,頁面明顯掉幀了。在7秒內,總共 scripting 的階段爲3秒, rendering 階段爲1956毫秒。

事實上,這種跑馬燈式的重複式效果,經過 animation 的方式也能夠輕鬆實現。 咱們優化上面的代碼,改成下面的代碼,經過 animation 動畫來控制過渡:

<div v-for="n in testArr">
      <div class="info-block"></div>
    </div>
複製代碼
export default {
  	data () {
  	  return {
            isShow: false,
            testArr: 1000
      }
    }
  }
複製代碼
.info-block {
  background-color: red;
  width: 300px;
  height: 100px;
  position: fixed;
  left: 10px;
  top: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: toggleShow 3s ease 0s infinite normal;
}

@keyframes toggleShow {
  0% {
    transform: translate(-400px);
  }
  10% {
    transform: translate(0,0);
  }
  80% {
    transform: translate(0,0);
  }
  100% {
    transform: translate(-400px);
  }
}
複製代碼

打開瀏覽器的開發者工具,能夠在 performance 裏面看到,頁面性能有了驚人的提高:

爲了進一步提高頁面的性能,咱們給過渡的元素增長一個 will-change 屬性,該元素就會被提高到 合成層 用GPU單獨渲染,這樣頁面性能就會有更大的提高。

優化懶加載(需考慮兼容性)

有一些頁面使用了懶加載,懶加載是經過綁定 scroll 事件一個回調事件,每一次調用一次回調事件,就會測量一次元素的位置,調用 getBoundingClientRect() 方法,從而計算出是否元素出如今了可視區。

// 懶加載庫中的代碼,判斷是否進入了可視區
const isInView = (el, threshold) => {
  const {top, height} = el.getBoundingClientRect()
  return top < clientHeight + threshold && top + height > -threshold
}

複製代碼

scroll 形成頁面性能降低

scroll 事件會被重複的觸發,每觸發一次就要測量一次元素的尺寸和位置。儘管對 scroll 的事件進行了節流的處理,但在低端安卓機上仍然會出現滑動不流暢的現象。

優化的思路是經過新增的api—— IntersectionObserver 來獲取元素是否進入了可視區。

使用intersection observer

intersection observer api 能夠去測量某一個dom節點和其餘節點,甚至是viewport的距離。

這個是實驗性的api,你應該查閱https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility查看其兼容性

在過去,檢測一個元素是否在可視區內,或者兩個元素之間的距離如何,是一個很是艱鉅的任務。 但獲取這些信息是很是必要的:

  1. 用於懶加載
  2. 用於無限加載,就是微博那種刷到底接着請求新數據能夠接着刷
  3. 檢測廣告的可見性

在過去,咱們須要不斷的調用 Element.getBoundingClientRect() 方法去獲取到咱們想拿到的信息,然而這些代碼會形成性能問題。

intersection observer api 能夠註冊回調函數,當咱們的目標元素,進入指定區域(好比說viewport,或者其餘的元素)時,回調函數會被觸發;

intersectionObserver 的語法

var handleFun = function() {}
  var boxElement = document.getElementById()
  
  var options = {
    root: null,
    rootMargin: "0px",
    threshold: 0.01
  };

  observer = new IntersectionObserver(handleFunc, options);
  observer.observe(boxElement);

複製代碼

基於IntersectionObserver的懶加載的庫

因而本身嘗試封裝了一個基於IntersectionObserver的懶加載的庫。

html

<img class="J_lazy-load" data-imgsrc="burger.png">
複製代碼

你也許注意到上面的代碼中,圖片文件沒有 src 屬性麼。這是由於它使用了稱爲 data-imgsrc 的 data 屬性來指向圖片源。咱們將使用這來加載圖片

js

function lazyLoad(domArr) {
	if ('IntersectionObserver' in window) {
		
		let createObserver = (dom) => {
			var fn = (arr) => {
				let target = arr[0].target
				if (arr[0].isIntersecting) {
					let imgsrc = target.dataset.imgsrc
					if (imgsrc) {
						target.setAttribute('src', imgsrc)
					}
					
					// 解除綁定觀察
					observer.unobserve(dom)
				}
			}
			
			var config = {
				root: null,
				rootMargin: '10px',
				threshold: 0.01
			}
			
			var observer =  new IntersectionObserver(fn, config)
			observer.observe(dom)
		}
		
		Array.prototype.slice(domArr)
		domArr.forEach(dom => {
			createObserver(dom)
		})
	}
}

複製代碼

這個庫的使用也很是簡單:

// 先引入
import {lazyLoad} from '../util/lazyload.js'

// 進行懶加載
let domArr = document.querySelectorAll('.J_lazy-load')
lazyLoad(domArr)

複製代碼

而後測試一下,發現能夠正常使用:

比較性能

傳統的懶加載 lazy-loder 的頁面性能以下:

在12秒內,存在紅顏色的掉幀現象,一些地方的幀率偏低(在devtool裏面是fps的綠色小山較高的地方),用於 scripting 階段的總共有600多ms.

使用intersetctionObserver以後的懶加載性能以下:

在12秒內,幀率比較平穩,用於 scripting 階段的時間只有60多ms了。

參考連接:

  1. hacks.mozilla.org/2017/08/ins…
  2. codeburst.io/how-browser…
  3. developer.mozilla.org/en-US/docs/…
  4. www.chromium.org/developers/…
  5. www.w3.org/TR/CSS22/vi…
  6. css性能優化
  7. render tree
相關文章
相關標籤/搜索