深入理解浏览器解析和执行过程
在我們公司的業(yè)務場景中,有很大一部分用戶是使用老款安卓機瀏覽頁面,這些老款安卓機性能較差,如果優(yōu)化不足,頁面的卡頓現象會更加明顯,此時頁面性能優(yōu)化的重要性就凸顯出現。優(yōu)化頁面的性能,需要對瀏覽器的渲染過程有深入的了解,針對瀏覽器的每一步環(huán)節(jié)進行優(yōu)化。
頁面高性能的判斷標準是 60fps。這是因為目前大多數設備的屏幕刷新率為 60 次/秒,也就是 60fps , 如果刷新率降低,也就是說出現了掉幀, 對于用戶來說,就是出現了卡頓的現象。
這就要求,頁面每一幀的渲染時間僅為16毫秒 (1 秒/ 60 = 16.66 毫秒)。但實際上,瀏覽器有其他工作要做,因此這一幀所有工作需要在 10毫秒內完成。如果工作沒有完成,幀率將下降,并且內容會在屏幕上抖動。 此現象通常稱為卡頓,會對用戶體驗產生負面影響。
瀏覽器渲染流程
瀏覽器一開始會從網絡層獲取請求文檔的內容,請求過來的數據是 Bytes,然后瀏覽器將其編譯成HTML的代碼。
但是我們寫出來的HTML代碼瀏覽器是看不懂的,所以需要進行解析。
渲染引擎解析 HTML 文檔,將各個dom標簽逐個轉化成“DOM tree”上的 DOM 節(jié)點。同時也會解析內部和外部的css, 解析為CSSOM tree, css tree和dom tree結合在一起生成了render tree。
render tree構建好之后,渲染引擎隨后會經歷一個layout的階段: 計算出每一個節(jié)點應該出現在屏幕上的確切坐標。
之后的階段被稱為paiting階段,渲染引擎會遍歷render tree, 然后由用戶界面后端層將每一個節(jié)點繪制出來。
最后一個階段是 composite 階段,這個階段是合并圖層。
瀏覽器內核
瀏覽器是一個極其復雜龐大的軟件。常見的瀏覽器有chrome, firefox。firefox是完全開源,Chrome不開源,但Chromium項目是部分開源。
Chromium和Chrome之間的關系類似于嘗鮮版和正式版的關系,Chromium會有很多新的不穩(wěn)定的特性,待成熟穩(wěn)定后會應用到Chrome。
瀏覽器功能有很多,包括網絡、資源管理、網頁瀏覽、多頁面管理、插件和擴展、書簽管理、歷史記錄管理、設置管理、下載管理、賬戶和同步、安全機制、隱私管理、外觀主題、開發(fā)者工具等。
因此瀏覽器內部被劃分為不同的模塊。其中和頁面渲染相關的,是下圖中虛線框的部分渲染引擎。
渲染引擎的作用是將頁面轉變成可視化的圖像結果。
目前,主流的渲染引擎包括Trident、Gecko和WebKit,它們分別是IE、火狐和Chrome的內核(2013年,Google宣布了Blink內核,它其實是從WebKit復制出去的),其中占有率最高的是 WebKit。
WebKit
最早,蘋果公司和KDE開源社區(qū)產生了分歧,復制出一個開源的項目,就是WebKit。
WebKit被很多瀏覽器采用作為內核,其中就包括goole的chrome。
后來google公司又和蘋果公司產生了分歧,google從webkit中復制出一個blink項目。
因此,blink內核和webkit內核沒有特別的不同,因此很多老外會借用 chromium的實現來理解webkit的技術內幕,也是完全可以的。
瀏覽器源碼
瀏覽器的代碼非常的龐大,曾經有人嘗試閱讀Chromium項目的源碼,git clone 到本地發(fā)現有10個G,光編譯時間就3個小時(據說火狐瀏覽器編譯需要更多的時間,大約為6個小時)。因此關于瀏覽器內部究竟是如何運作的,大部分的分享是瀏覽器廠商參與研發(fā)的內部員工。
國外有個非常有毅力的工程師Tali Garsiel 花費了n年的時間探究了瀏覽器的內幕,本文關于瀏覽器內部工作原理的介紹,主要整理自她的博客how browser work , 和其他人的一些分享。
國內關于瀏覽器技術內幕主要有《WebKit技術內幕》
下面,我們將針對瀏覽器渲染的環(huán)節(jié),深入理解瀏覽器內核做了哪些事情,逐一的介紹如何去進行前端頁面的優(yōu)化。
瀏覽器渲染第一步:解析
解析是瀏覽器渲染引擎中第一個環(huán)節(jié)。我們先大致了解一下解析到底是怎么一回事。
什么是解析
通俗來講,解析文檔是指將文檔轉化成為有意義的結構,好讓代碼去使用他們。
以上圖為例,右邊就是解析好的樹狀結構,這個結構就可以“喂“給其他的程序, 然后其他的程序就可以利用這個結構,生成一些計算的結果。
解析的過程可以分成兩個子過程:lexical analysis(詞法分析)和syntax analysis(句法分析)。
lexical analysis(詞法分析)
lexical analysis 被稱為詞法分析的過程,有的文章也稱為 tokenization,其實就是把輸入的內容分為不同的tokens(標記),tokens是最小的組成部分,tokens就像是人類語言中的一堆詞匯。比如說,我們對一句英文進行l(wèi)exical 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是應用語言句法中的規(guī)則, 簡單來說,就是判斷一串tokens組成的句子是不是正確的。
如果我說:“我吃飯工作完了”, 這句話是不符合syntax analysis的,雖然里面的每一個token都是正確的,但是不符合語法規(guī)范。需要注意的是,符合語法正確 的句子不一定是符合語義正確的。比如說,“一個綠色的夢想沉沉的睡去了”,從語法的角度來講,形容詞 + 主語 + 副詞 + 動詞沒有問題,但是語義上卻是什么鬼。
負責syntax analysis工作的是**parser**,解析是一個不斷往返的過程。
如下圖所示,parser向lexer要一個新的token,lexer會返回一個token, parser拿到token之后,會嘗試將這個token與某條語法規(guī)則進行匹配。
如果該token匹配上了語法規(guī)則,parser會將一個對應的節(jié)點添加到 parse tree (解析樹,如果是html就是dom tree,如果是css就是 cssom tree)中,然后繼續(xù)問parser要下一個node。
當然,也有可能該tokens沒有匹配上語法規(guī)則,parser會將tokens暫時保存,然后繼續(xù)問lexer要tokens, 直至找到可與所有內部存儲的標記匹配的規(guī)則。如果找不到任何匹配規(guī)則,parser就會引發(fā)一個異常。這意味著文檔無效,包含語法錯誤。
syntax analysis 的輸出結果是parse tree, parse tree 的結構表示了句法結構。比如說我們輸入"John hit the ball"作為一句話,那么 syntax analysis 的結果就是:
一旦我們拿到了parse tree, 還有最后一步工作沒有做,那就是:translation,還有一些博客將這個過程成為 compilation / transpilation / interpretation
Lexicons 和 Syntaxes
上面提到了lexer 和 parser 這兩個用于解析工具,我們通常不會自己寫,而是用現有的工具去生成。我們需要提供一個語言的 lexicon 和 syntaxes ,才可以生成相應的 lexer 和 parser 。
webkit 使用的 lexer 和 parser 是 Flex 和 Bison 。
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 通常是被一個叫無上下文語法所定義,關于無上下文語法可以點擊這個鏈接,反正只需要知道,無上下文語法要比常規(guī)的語法更復雜就好了。
BNF范式
非科班出身的前端可能不了解 BNF 范式(說的就是我 --),它是一種形式化符號來描述給定語言的語法。
它的內容大致為:
下面是用BNF來定義的Java語言中的For語句的實例。
FOR_STATEMENT ::= "for" "(" ( variable_declaration | ( expression ";" ) | ";" ) [ expression ] ";" [ expression ] ")" statement 復制代碼BNF 的誕生還是挺有意思的一件事情, 有了BNF才有了真正意義上的計算機語言。巴科斯范式直到今天,仍然是個迷,巴科斯是如何想到的
小結
我們現在對解析過程有了一個大致的了解,總結成一張圖就是這樣:
對解析(parse)有了初步的了解之后,我們看一下HTML的解析過程。
解析HTML
HTML是不規(guī)范的,我們在寫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 標簽的容錯性很高,需要上下文敏感的語法。
比如說對于下面兩段代碼:
<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. 復制代碼第一段是規(guī)范的html代碼,第二段代碼有非常多的錯誤,但是這兩段代碼在瀏覽器中都是大致相同的結構:
上面兩處代碼渲染出來的唯一的不同就是,正確的html會在頭部有<!DOCTYPE html>, 這行代碼會觸發(fā)瀏覽器的標準模式。
所以你看,html 的容錯性是非常高的,這樣是有代價的,這增加了解析的困難,讓詞法解析解析更加困難。
DOM Tree
HTML 解析出來的產物,經過加工,就得到了DOM Tree。
對于下面這種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 Tree。 CSSOM 被稱為“css 對象模型”。
CSSOM Tree 對外定義接口,可以通過js去獲取和修改其中的內容。開發(fā)者可以通過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,如下圖所示:
可以看到,CSSOM是一個對象,其中有三個屬性,均是 CSSStylelSheet 對象。CSSStylelSheet 對象用于表示每個樣式表。由于我們在document里面引入了一個外部樣式表,和兩個內聯樣式表,所以CSSOM對象中包含了3個CSSStylelSheet對象。
CSSStyleSheet
CSSStylelSheet對象又長什么樣子呢?如下圖所示:
CSSStyleSheet 對象主要包括下面的屬性:
-
type
字符串 “text/css”
-
href
表示該樣式表的來源,如果是內聯樣式,則href值為null
-
parentStyleSheet
父節(jié)點的styleSheet
-
ownerNode
該樣式表所匹配到的DOM節(jié)點,如果沒有則為空
-
ownerRule
父親節(jié)點的styleSheet中的樣式對本節(jié)點的合并
-
media
該樣式表中相關聯的 MediaList
-
title
style 標簽的title屬性,不常見
<style title="papaya whip">body { background: #ffefd5; } </style> 復制代碼 -
disabled
是否禁用該樣式表,可通過js控制該屬性,從而控制頁面是否應用該樣式表
樣式表的解析
瀏覽器的渲染引擎是從上往下進行解析的。
當渲染引擎遇到 <style> 節(jié)點的時候,會立馬暫停解析html, 轉而解析CSS規(guī)則,一旦CSS規(guī)則解析完成,渲染引擎會繼續(xù)解析html
當渲染引擎遇到 <link>節(jié)點的時候,瀏覽器的網絡組件會發(fā)起對 style 文件的請求,同時渲染引擎不會暫停,而是繼續(xù)往下解析。等到 style 文件從服務器傳輸到瀏覽器的時候,渲染引擎立馬暫停解析html, 轉而解析CSS規(guī)則,一旦CSS規(guī)則解析完成,渲染引擎會繼續(xù)解析html。
可以聯想一下script的解析。
當渲染引擎遇到 <script> 節(jié)點的時候,會立馬暫停解析html。
如果這個 <script> 節(jié)點是內聯,則 JS 引擎會馬上執(zhí)行js代碼,同時渲染引擎也暫停了工作。什么時候等 JS 代碼執(zhí)行完了,什么時候渲染引擎重新繼續(xù)工作。如果JS 代碼執(zhí)行不完,那渲染引擎就繼續(xù)等著吧。
如果這個 <script> 節(jié)點是外鏈的,瀏覽器的網絡組件會發(fā)起對 script 文件的請求,渲染引擎也暫停了執(zhí)行。什么時候等 JS 代碼下載完畢,并且執(zhí)行完了,什么時候渲染引擎重新繼續(xù)工作。
在2011年的時候,瀏覽器廠商推出了“推測性”解析的概念。
“推測性”解析就是,當讓渲染引擎干等著js代碼下載和運行的時候,會起一個第三個進程,繼續(xù)解析剩下的html。當js代碼下載好了,準備開始執(zhí)行js代碼的時候,第三個進程就會馬上發(fā)起對剩下html所引用的資源——圖片,樣式表和js代碼的請求。
這樣就節(jié)省了之后加載和解析時間。
被稱為“推測性”解析是因為,前面的js代碼存在一定的概率修改DOM節(jié)點,有可能會讓后面的DOM節(jié)點消逝,那么我們的工作就白費了。瀏覽器“推測”這樣的發(fā)生的概率比較小。
讓渲染引擎干等著不工作是非常低效率的,所以雅虎軍規(guī)會讓把 script 標簽放在body的底部。
言歸正傳,樣式表放在head的前邊,有兩個原因:
Render Tree
當瀏覽器忙著構建DOM Tree和 CSSOM Tree的時候, 瀏覽器同時將兩者結合生成Render Tree。也就是說,瀏覽器構建DOM Tree和 CSSOM 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,其中的每一個構成的節(jié)點都是 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 Tree 和 Render Object Tree 之間的關系是什么樣的?
Render Object Tree 并不嚴格等于Dom Tree,先看一張DOM Tree 和 Render Object Tree的直觀的對比圖:
上面左側DOM tree的節(jié)點對應右側Render Object Tree上的節(jié)點。細心的你會注意到,上圖左側的DOM Tree中的HTMLDivElement 會變成RenderBlock, HTMLSpanElement 會變成RenderInline,也就是說,DOM節(jié)點對應的 render object 節(jié)點并不一樣。
DOM節(jié)點對應的 render object 節(jié)點并不一樣分這幾種情況:
display : none 的DOM 節(jié)點沒有對應的 Render Object Tree 的節(jié)點
這里的display:none 屬性,有可能是我們在CSS里面設置的,也有可能是瀏覽器默認的添加的屬性。比如說下面的元素就會有默認的display:none的屬性。
- <head>
- <meta>
- <link>
- <script>
下面的各個DOM元素,會對應多個Render Object Tree的節(jié)點
-
<input type="**color**">
-
<input type="**date**">
-
<input type="**file**">
-
<input type="**range**">
-
<input type="**radio**">
-
<input type="**checkbox**">
-
<select>
比如說,<input type="range">?就會有兩個renderer:
脫離文檔流的情況,要么是float, 要么是position: absolute / fixed。
比如說對于下面的結構:
<body> <div><p>Lorem ipsum</p></div> </body> 復制代碼? 它的 DOM tree和 Render Tree 如下圖所示:
? 如果增加脫離文檔流的樣式,如下:
p {position: absolute; } 復制代碼? 情況就會變成下面這樣:
<p> 節(jié)點對應的 Render Tree 的節(jié)點,從父節(jié)點脫離出來,掛到了頂部的RenderView 節(jié)點下面。
為什么脫離了文檔流的節(jié)點,在 Render Object Tree中的結構不同?脫離了文檔流的節(jié)點在構建Render Object Tree又是如何處理的?會在下面的內容中介紹。
Render Object Tree 上的節(jié)點
render object tree 是由 render object 節(jié)點構成的。render object 節(jié)點在不同的瀏覽器叫法不同,在webkit中被稱為 renderer, 或者 被稱為 render objects, 在firfox中,被稱為frames。
render object 的節(jié)點的類是 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的節(jié)點所指向的那個Dom節(jié)點RenderStyle* style; // 這個render tree節(jié)點的計算出來的樣式RenderLayer* containingLayer; // 包含這個 render tree 的 z-index layer } 復制代碼RenderBox?是RenderObject的一個子類,它主要是負責DOM樹上的每一個節(jié)點的盒模型。
RenderBox?包括一些計算好尺寸的信息,比如說:
- height
- width
- padding
- border
- margin
- clientLeft
- clientTop
- clientWidth
- clientHeight
- offsetLeft
- offsetTop
- offsetWidth
- offsetHeight
- scrollLeft
- scrollTop
- scrollWidth
- scrollHeight
render object 的節(jié)點的作用如下:
-
負責 layout 和 paint
-
負責查詢DOM元素查詢尺寸API
比如說獲取offsetHeight, offsetWidth的屬性
render object 節(jié)點的類型
我們在CSS中接觸過文檔流的概念,文檔流中的元素分為塊狀元素和行內元素,比如說div是塊狀元素,span是行內元素。塊狀元素和行內元素在文檔流中的表現不同,就是在這里決定的。
Render Object 的節(jié)點類型分為下面幾種:
RenderBlock
display: block 的DOM節(jié)點對應的render object節(jié)點類型為RenderBlock
RenderInline
display:inline 的DOM節(jié)點對應的render object節(jié)點類型為RenderInline
RenderReplaced
可能我們之前聽說過“替換元素” 的概念,比如說常見的“替換元素”有下面:
- <iframe>
- <video>
- <embed>
- <img>
為啥被稱為“替換元素”,是因為他們的內容會被一個獨立于HTML/CSS上下文的外部資源所替代。
“替代元素” 的DOM節(jié)點對應的render object 節(jié)點類型為RenderReplaced
RenderTable
<table> 元素的DOM節(jié)點對應的render object 節(jié)點類型為 RenderTable
RenderText
文本內容的DOM節(jié)點對應的render object 的節(jié)點類型為 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 的節(jié)點之間的關系組合并不是沒有準則的,在我們寫出嵌套不規(guī)范的HTML時,渲染引擎幫我們做了很多事情。
Anonymous renderers
render object tree 遵守2個準則:
- 在文檔流中的塊狀元素的子節(jié)點,要么都是塊狀元素,要么都是行內元素。
- 在文檔流中的行內元素的子節(jié)點,只能都是行內元素。
anonymounse renderers(匿名的render object 節(jié)點)就是用于處理不遵守這兩種規(guī)則的代碼的,如果出現不符合這兩個準則的情況,比如說下面:
上面的代碼中,最外層的div節(jié)點有兩個子節(jié)點,第一個子節(jié)點是行內元素,第二個子節(jié)點是塊狀元素。render object tree 中會構建一個anonymounse renderer去包裹 text 節(jié)點,因此上面的代碼變成了下面的:
<div><anonymous block>Some text</anonymous block><div>Some more text</div> </div> 復制代碼上面的代碼中,render object tree需要做更多的事情去修復這種糟糕的DOM tree: 三個anonymounse renderers會被創(chuàng)建,上面的代碼會被分割成三段,被三個匿名的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 的節(jié)點中,他們通過一種叫*continuation chain(延續(xù)鏈)*的機制來鏈接。
負責上面遞歸拆分行內元素的生產*continuation chain(延續(xù)鏈)*的方法被稱為 splitFlow。
因此,一旦你寫出了不符合規(guī)范的html結構, 在構建render object tree時就需要更多的工作去糾正,從而造成頁面性能的下降。
構建 Render Object Tree
Gecko 和 WebKit 采用了不同的方案來構建 Render Tree。
Gecko 是把樣式計算和構建Render Object Tree 的工作代理到 FrameConstructor 對象上。而 webkit 采用的方案是,每一個DOM節(jié)點自己計算自己的樣式,并且構建自己 的Render object tree 對應的節(jié)點。
Gecko 針對DOM的更新增加了一個 listener,當DOM 更新的時候,更新的DOM節(jié)點被傳到一個指定的對象FrameConstructor, 這個FrameConstructor 會為 DOM 節(jié)點計算樣式,同時為這個DOM節(jié)點創(chuàng)建一個合適的 Render Object Tree節(jié)點。
WebKit構建 Render Object tree 的過程被稱為attachment, 每一個DOM節(jié)點被賦予一個 attach() 方法,這是一個同步的方法,當每一個DOM節(jié)點被插入DOM樹的時候, 該DOM節(jié)點的attach()方法就會被調用。
樣式計算
在構建Render Object Tree的時候,需要進行樣式計算,也就是Render Tree每一個節(jié)點都需要有一個visual information的信息,才可以被繪制在屏幕上,這就需要樣式計算這一過程。
而樣式計算需要兩部分“原材料”:
DOM Tree在HTML解析之后就可以拿到了,一堆樣式規(guī)則可以來自下面:
- 瀏覽器默認的樣式
- 外鏈樣式
- 內聯樣式
- DOM節(jié)點上的style屬性
那么樣式規(guī)則是如何構成的呢?
- 樣式表是一堆 規(guī)則(rules)的集合;
- 當然也不光都是 規(guī)則(rules), 還會有一些奇怪的東西:@import, @media, @namespace 等等
- 一個**規(guī)則(rules)是由選擇器(selector)和聲明塊(declaration block)**構成的
- **聲明塊(declaration block)由一堆聲明(declaration)**加中括號構成
- **聲明(declaration ** 由 property 和 value 構成。
樣式計算存在以下三個難點:
下面我們介紹這個三個難點是如何解決的。
樣式規(guī)則的應用順序
某一個DOM節(jié)點上可能有多個規(guī)則,比如下下面:
div p {color: goldenrod; } p {color: red; }復制代碼那么這個DOM節(jié)點究竟用的是哪個規(guī)則?
規(guī)則的權重是:先看 order , 然后再算specificity, 最后再看哪個規(guī)則靠的更近。
order
order的權重從高到底:
Specificity
Specificity是一個相加起來的值
#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */復制代碼第一位的數值(a)
是否有DOM節(jié)點上style屬性的值,有則是1,否則是0
第二位的數值 (b)
id選擇器的數量之和
第三位的數值 (c)
class選擇器,屬性選擇器,偽類選擇器個數之和
第四位的數值 (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每一個節(jié)點上都需要存儲全部的CSS樣式,那些沒有被指定的樣式,其值為繼承父節(jié)點的樣式,或者是瀏覽器的默認樣式,或者干脆是個空值。
webkit 和 gecko 采用了不同的解決方案。
webkit:共享樣式數據
WebKit 的解決方案是,節(jié)點們會引用RenderStyle對象。這些對象在以下情況下可以由不同的DOM節(jié)點共享,從而節(jié)省空間和提高性能。
- 這些節(jié)點是同級關系
- 這些節(jié)點有相同的偽類狀態(tài):hover、:active、:focus
- 這些節(jié)點都沒有id
- 這些節(jié)點有相同的tag名稱
- 這些節(jié)點有相同的class
- 這些節(jié)點都沒有通過style屬性設置的樣式
- 這些節(jié)點沒有一個是使用 兄弟選擇器的,比如說: div + p,?div ~ 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。如下表所示:
上圖中的,computed style 里就不用存儲CSS全部的200多個屬性,而是保存著對這些 style struct對象的引用。
這樣一來,一些具有相同屬性的DOM 節(jié)點就可以引用相同的 style struct, 不僅如此,因為子節(jié)點有一些屬性會繼承父節(jié)點,那么保存這些屬性的 style struct 就會被父節(jié)點和子節(jié)點所共享。
匹配元素會影響性能
對于每一個DOM節(jié)點,css引擎需要去遍歷所有的css規(guī)則看是否匹配。對于大部分的DOM節(jié)點,css規(guī)則的匹配并不會發(fā)生改動。
比如說,用戶把鼠標hover到一個父元素上面,這個父元素的css規(guī)則匹配是發(fā)生了變化,我們不僅僅要重新計算這個父元素的樣式,還需要重新計算這個父元素的子元素的樣式(因為要處理繼承的樣式),但是能匹配這些子元素的樣式規(guī)則,是不會變的。
如果我們能記下來,有哪些selector可以匹配到就好了。
為了優(yōu)化這一點,CSS 引擎在進行 selector 匹配時,會根據權重的順序把他們排成一串,然后把這一串加到右邊的 CSS rule tree 上面。
CSS引擎希望右邊CSS rule tree 的分支數越少越好,因此會將新加入的一串盡量的合并到已有的分支,所以上面的過程會是下面這樣的:
然后遍歷每一個DOM節(jié)點去找能匹配到的CSS Rule Tree的分支,從CSS rule Tree的底部開始,一路向上開始匹配,直到找到對應的那一條 style rule Tree分支。這就是人們口中常說的,css選擇器是從右邊匹配的。
當瀏覽器因為某種原因(用戶交互,js修改DOM)進行重新渲染的時候,CSS引擎會快速檢查一下,對父節(jié)點的改動是否會影響到子節(jié)點的 selector 匹配,如果不影響,CSS引擎就直接拿到每一個子節(jié)點對CSS rule Tree 對應那個分支的指針,節(jié)省掉匹配選擇器的時間。
盡管如此,我們還是需要在第一次遍歷每一個DOM節(jié)點去找到對應的CSS Rule Tree的分支。如果我們有10000個相同的節(jié)點,就需要遍歷10000次。
Gecko 和 Webkit 都對此進行了優(yōu)化,在遍歷完一個節(jié)點之后,會把計算好的樣式放到緩存中,在遍歷下一個節(jié)點之前,會做一個判斷,看是否可以復用緩存中的樣式。
這個判斷包括一下幾點:
兩個節(jié)點是否有相同的id, class
兩個節(jié)點是否有相同的style 屬性
兩個節(jié)點對應的父親節(jié)點是否共享一份計算好的樣式,那該兩個節(jié)點繼承的樣式也是相同的。
解析階段如何優(yōu)化
更加符合規(guī)范的html結構
上面在構建render object tree 的過程中,會額外做很多工作處理我們不符合規(guī)范的DOM 結構,比如說,調用splitflow 方法分割代碼,用 anonymous renderBlock 包裹不符合規(guī)范的節(jié)點。
之前我們都聽過建議:“要編寫更有語義,更符合規(guī)范的html結構“,原因就在于此,可以讓渲染引擎做更少的事情。
下面是模擬一種不不符合規(guī)范的情況:
<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 textMore italic and bold text</b> More italic text</i>復制代碼在控制臺里面,設置cpu 為6x slowdown,然后記錄渲染數據如下:
其中花費了 12888ms 進行了rendering 過程。
如果我們對html代碼僅僅做幾處修改,在不考慮css優(yōu)化、樣式優(yōu)化的前提下:
<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 textMore italic and bold text</div></div>復制代碼在控制臺里面,設置cpu 為6x slowdown,然后記錄渲染數據如下:
可以發(fā)現,render 階段的渲染時間為11506ms,rendering 階段渲染的時間相比于12888ms減少了1382ms,時間縮短了12%
一次測量可能有誤差,但無論進行多次測量,都會發(fā)現第二種的代碼的渲染時間要小于第一種代碼的渲染時間。
選擇器的優(yōu)化
不同的選擇器,匹配的效率會有差距,但是差距不大。
我們用一個有1000個DOM節(jié)點的頁面來測試,分別在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 selectora: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 singlea {color: red;}*//*17. Element doublediv a {color: red;}*//*18. Element treblediv ul a {color: red;}*//*19. Element treble pseudodiv ul a:after; {content: "after";color: red;}*//*20. Single class.link {color: red;} 復制代碼測試的結果如下:
| 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 |
解釋
在瀏覽器的引擎內部,這些選擇器會被重新的拆分,組合,優(yōu)化,編譯。而不同的瀏覽器內核采用不同的方案,所以幾乎沒有辦法預測,選擇器的優(yōu)化究竟能帶來多少收益。
結論:
合理的使用選擇器,比如說層級更少的class,的確會提高匹配的速度,但是速度的提高是有限的 。
如果你通過dev tool 發(fā)現匹配選擇器的確是瓶頸,那么就選擇優(yōu)化它。
精簡沒有用的樣式代碼
大量無用代碼會拖慢瀏覽器的解析速度。
用一個3000行的無用css樣式表和1500行的無用樣式表,進行測試:
| 3000 | 64.4 | 237.6 | 74.2 | 436.8 | 1714.6 |
| 1500 | 51.6 | 142.8 | 65.4 | 358.6 | 1412.4 |
對于火狐來說,在其他環(huán)節(jié)一致的情況下,頁面渲染的速度幾乎提升了一倍
盡管現在的慣例是把css 打包成一個巨大單一的css文件。這樣做的確是有好處的,減少http請求的數量。但是拆分css文件可以讓加載速度更快,瀏覽器的解析速度更快。
這一項的優(yōu)化是非常顯著的,通常可以省下來 2ms ~ 300ms的時間。
精簡的過程可以使用uncss 工具來自動化的完成。
瀏覽器渲染第二步:layout
在上一節(jié)我們提到了 render object tree, render object 的節(jié)點第一次被創(chuàng)建然后添加到 render object tree時,它身上沒有關于位置和尺寸的信息。接下來,確定每一個render object的位置和尺寸的過程被稱為layout。
我們能在不同的文章中看到不同的名詞: 布局 ,layout , 回流 , reflow , 這些名詞說的都是一回事,不同瀏覽器的叫法不同。
每一個renderer節(jié)點 都有l(wèi)ayout 方法。 在構建renderer節(jié)點的時候就聲明了這個方法:
class RenderObject {virtual void layout();virtual void paint(PaintInfo);virtual void rect repaintRect();Node* node; // 這個render tree的節(jié)點所指向的那個Dom節(jié)點RenderStyle* style; // 這個render tree節(jié)點的計算出來的樣式RenderLayer* containingLayer; // 包含這個 render tree 的 z-index layer } 復制代碼layout ()是一個遞歸的過程。layout 過程究竟是誰來負責的呢? 一個名為 FrameView 的 class。
FrameView 可以運行下面兩種類型的 layout :
全局layout
render tree 的根節(jié)點自身的layout方法被調用,然后整個render tree 被更新。
局部layout
只是區(qū)域性的更新,只適用于某個分支的改動不會影響到周圍的分支。
目前局部layout只會在 text 更新的時候使用
Dirty Bits
在layout 階段,采用一種稱為 Dirty Bits 的機制去判斷一個節(jié)點是否需要layout。當一個新的節(jié)點被插到tree中時,它不僅僅“弄臟“了它自身,還“弄臟“了相關的父節(jié)點(the relevant ancestor chain,下面會介紹)。有沒有被“弄臟”是通過設置bits (set bits)來標識的。
bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout || m_posChildNeedsLayout; } 復制代碼上面 needsLayout 為 true 有三種情況:
selfNeedsLayout
Rederer 自身是 “臟”的。當一個 rederer 自身被設置為“臟”的,它相關的父親節(jié)點也會被設置一個標識來指出它們有一個“臟”的子節(jié)點
posChildNeedsLayout
設置了postion不為static的子節(jié)點被弄臟了
normalChildNeedsLayout
在文檔流中的子節(jié)點被弄臟了
上面之所以要區(qū)分子節(jié)點是否在文檔流中,是為了layout過程的優(yōu)化。
Containing Block (包含塊)
上面提到了相關父節(jié)點(the relevant ancestor chain),那么究竟是如何判斷哪個節(jié)點是 **相關父節(jié)點 **?
答案就是通過 containing block.
Container Block(包含塊) 身份有兩個
子節(jié)點的相關的父節(jié)點
子節(jié)點的相對坐標系
子節(jié)點都有 XPos 和 YPos 的坐標,這些坐標都是相對于他們的Containing Block (包含塊)而言的。
下面介紹Container Block(包含塊) 概念。
包含塊的定義
通俗來講,Container Block 是決定子節(jié)點位置的父節(jié)點。每個子節(jié)點的位置都是相對于其container block來計算的。更詳細的信息可以點這個 css2.1 官方的解釋點這里
有一種特殊的containing block —— initial containing block (最初的container block)。
當Docuement 節(jié)點上的 renderer() 方法被調用時,會返回一個節(jié)點對象為render tree 的根節(jié)點,被稱作 RenderView, RenderView 對應的containing bock 就是 initial containing block。
initial containing block 的尺寸永遠是viewport的尺寸,且永遠是相對于整個文檔的 position(0,0) 的位置。下面是圖示:
黑色的框代表的是 initial containing block (最初的container block) , 灰色的框表示整個 document。當document往下滾動的時候, initial containing block (最初的container block) 就會被移出了屏幕。 initial containing block (最初的container block) 始終在document 的頂部,并且大小始終是 viewport 的尺寸。
那么render Tree上的節(jié)點,它們各自的 containing block 是什么?
-
根節(jié)點的 containing block 始終是 RenderView
-
如果一個renderer節(jié)點的css postion 的值為 relative 或 static,則其 containing block 為最近的父節(jié)點
-
如果一個renderer節(jié)點的css postion 的值為 absolute, 則其containing block 為最近的 css postion 的值不為static 的父節(jié)點。如果這樣的父節(jié)點不存在,則為 RenderView,也就是根節(jié)點的containing block
-
如果一個renderer節(jié)點的css postion 的值為 fixed。這個情況有一些特殊,因為 W3C 標準和 webkit core 介紹的不一樣。W3C 最新的標準認為css postion 的值為 fixed的renderer節(jié)點的containing block是viewport ,原文如下:
而webkit core 認為css postion 的值為 fixed的renderer節(jié)點的containing block是RenderView。RenderView并不會表現的和viewport一樣,但是RenderView會根據頁面滾動的距離算出css postion 的值為 fixed的renderer節(jié)點的位置。這是因為單獨為viewport 生成一個renderer 節(jié)點并不簡單。原文如下:
render tree 有兩個方法用來判斷 renderer 的position:
bool isPositioned() const; // absolute or fixed positioning bool isRelPositioned() const; // relative positioning復制代碼render tree 有一個方法用來獲取某一個塊狀 rederer 的containing block(相對父節(jié)點)
RenderBlock* containingBlock() const復制代碼render tree 還有一個方法是兼容了行內元素獲取相對父節(jié)點的方法,用來代替containingBlock (因為containingBlock只適用于塊狀元素)
RenderObject* container() const復制代碼當一個 renderer 被標記為需要 layout的時候,就會通過container()找到相對父節(jié)點,把isPositioned 的狀態(tài)傳遞給相對父節(jié)點。如果 renderer 的position是absolute 或 fixed ,則相對父節(jié)點的posChildNeedsLayout為true,如果 renderer的position 是 static 或 relative , 則相對父節(jié)點的 normalChildNeedsLayout 為 true。
會觸發(fā)layout 的屬性
盒子模型相關的屬性
-
width
-
height
-
padding
-
margin
-
border
-
display
-
……
定位屬性和浮動
- top
- bottom
- left
- right
- position
- float
- clear
節(jié)點內部的文字結構
- text - aligh
- overflow
- font-weight
- font- family
- font-size
- line-height
上面只是一部分,更全部的可以點擊 csstriggers 來查閱;
csstrigger 里面需要注意的有幾點。
opacity的改動,在blink內核和Gecko內核上不會觸發(fā)layout 和 repaint
transform的改動,在blink內核和Gecko內核上不會觸發(fā)layout 和 repaint
visibility 的改動,在Gecko 內核上不會觸發(fā) layout repaint, 和 composite
會觸發(fā)layout 的方法
幾乎任何測量元素的寬度,高度,和位置的方法都會不可避免的觸發(fā)reflow, 包括但是不限于:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- and a lot more…
如何避免重復Layout
不要頻繁的增刪改查DOM
不要頻繁的修改默認根字體大小
不要一條條去修改DOM樣式,而是通過切換className
雖然切換className 也會造成性能上的影響,但是次數上減少了。
“離線”修改DOM
比如說一定要修改這個dom節(jié)點100次,那么先把dom的display設置為 none ( 僅僅會觸發(fā)一次回流 )
使用flexbox
老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上 Floxbox布局模型用流式布局的方式將元素定位到屏幕上,flex性能更好。
不要使用table
使用table布局哪怕一個很小的改動都會造成重新布局
避免強制性的同步layout
layout根據區(qū)域來劃分的,分為全局性layout, 和局部的layout。比如說修改根字體的大小,會觸發(fā)全局性layout。
全局性layout是同步的,會立刻馬上被執(zhí)行,而局部性的layout是異步的,分批次的。瀏覽器會嘗試合并多次局部性的layout為一次,然后異步的執(zhí)行一次,從而提高效率。
但是js一些操作會觸發(fā)強制性的同步布局,從而影響頁面性能,比如說讀取 offsetHeight、offsetWidth 值的時候。
瀏覽器渲染第三步:paint
第三個階段是paint 階段
會觸發(fā)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
如何優(yōu)化
使用transform代替top, left 的變化
使用transform不會觸發(fā)layout , 只會觸發(fā)paint。
如果你想頁面中做一些比較炫酷的效果,相信我,transform可以滿足你的需求。
// 位置的變換 transform: translate(1px,2px)// 大小的變換 transform: scale(1.2)復制代碼使用opacity 來代替 visibility
因為 visibility屬性會觸發(fā)重繪,而opacity 則不會觸發(fā)重繪
避免使用耗性能的屬性
可以點擊這個鏈接進行測試測試連接
.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; }復制代碼測試結果:
| Expensive Styles | 65.2 | 151.4 | 65.2 | 259.2 | 1923 |
需要注意的是,高耗css樣式如果不會頻繁的觸發(fā)回流和重繪,只會在頁面渲染的時候被執(zhí)行一次,那么對頁面的性能影響是有限的。如果頻繁的觸發(fā)回流和重繪,那么最基本的css樣式也會影響到頁面的性能。
那么哪些 css 樣式會造成頁面性能的問題呢?
- Border-radius
- Shadow
- gradients
- transform rotating
更多的內容請參考 連接
瀏覽器渲染第四步:composite
什么是合成層
上面幾個階段可以用下面一張圖來表示:
1. 從 Nodes 到 LayoutObjects
DOM 樹每個 Node 節(jié)點都有一個對應的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的內容。
2. 從 LayoutObjects 到 PaintLayers
有相同坐標的 LayoutObjects ,在同一個PaintLayer內。 根據創(chuàng)建PaintLayer 的原因不同,可以將其分為常見的 3 類:
- 根元素
- 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 應用動畫
- overflow 不為 visible
- 不需要 paint 的 PaintLayer,比如一個沒有視覺屬性(背景、顏色、陰影等)的空 div。
4. 從 PaintLayers 到 GraphicsLayers
某些特殊的paintLayer會被當成合成層,合成層擁有單獨的 GraphicsLayer,而其他不是合成層的paintLayer,則和其第一個擁有GraphicsLayer 父層公用一個。
每個 GraphicsLayer 都有一個GraphicsContext,GraphicsContext 會負責輸出該層的位圖,位圖是存儲在共享內存中,作為紋理上傳到 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 重疊也會造成提升合成層渲染? 圖層之間有重疊關系,需要按照順序合并圖層。
如何優(yōu)化
如果把一個頻繁修改的dom元素,抽出一個單獨的圖層,然后這個元素的layout, paint 階段都會在這個圖層進行,從而減少對其他元素的影響。
使用will-change 或者 transform3d
使用 will-change 或者 transform3d
1. will-change: transform/opacity2. transform3d(0,0,0,)復制代碼使用加速視頻解碼的節(jié)點
因為視頻中的每一幀都是在動的,所以視頻的區(qū)域,瀏覽器每一幀都需要重繪。所以瀏覽器會自己優(yōu)化,把這個區(qū)域的給抽出一個單獨的圖層
擁有3D(webgl) 上下文或者加速的2D上下文的節(jié)點
混合插件(flash)
如果某一個元素,通過z-index在復合層上面渲染,則該元素也會被提升到復合層
需要注意的是,gif 圖片雖然也變化很頻繁,但是 img 標簽不會被單獨的提到一個復合層,所以我們需要單獨的提到一個獨立獨立的圖層之類。
composite更詳盡的知識可以了解下面這個博客: 《GPU Accelerated Compositing in Chrome》
頁面性能優(yōu)化實踐
Bounce-btn優(yōu)化
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)復制代碼可以發(fā)現這樣的性能是非常差的,我們打開dev-tool的paint flashing, 發(fā)現重新渲染的區(qū)域如綠色的區(qū)域所示:
而此時的性能是,1000ms 的時間內,layout階段花費了29.9ms占了18.6%
這個其實有兩個地方,第一是,bounce btn 這個元素被js 修改了width 、height 這些屬性,從而觸發(fā)了自身layout ——> repaint ——> composite。第二是,bounce btn 沒有脫離文檔流,它自身布局的變化,影響到了它下面的元素的布局,從而導致下面元素也觸發(fā)了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)復制代碼頁面性能得到了提高:
重新渲染的區(qū)域只有它自身了。此時的性能是,1000ms 的時間內,沒有存在layout階段,
如果繼續(xù)優(yōu)化,我們通過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);}}復制代碼頁面中沒有重新渲染的區(qū)域:
并且頁面性能幾乎沒有受到任何影響,不會重新經歷 layout ——> repaint ——> composite.
所以,對于這種動效,優(yōu)先選擇 CSS animation > transform 修改 scale > 絕對定位 修改width > 文檔流中修改width
跑馬燈的優(yōu)化
跑馬燈的動效是:每隔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節(jié)點的class來實現。
<transision> 影響頁面性能
一方面, v-if 會修改dom節(jié)點的結構,修改dom節(jié)點會造成瀏覽器重走一遍 layout 階段,也就是重排。另一方面,dom節(jié)點的class被不停的修改,也會導致瀏覽器的重排現象,因此頁面性能會比較大的受到影響。
若頁面中 <transition> 控制的節(jié)點過多時,頁面的性能就會比較受影響。
為了證明,下面代碼模擬了一種極端的情況:
<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瀏覽器的開發(fā)者工具,然后在 performance 一欄中記錄分析性能,如下圖所示:
可以發(fā)現,頁面明顯掉幀了。在7秒內,總共 scripting 的階段為3秒, rendering 階段為1956毫秒。
事實上,這種跑馬燈式的重復式效果,通過 animation 的方式也可以輕松實現。 我們優(yōu)化上面的代碼,改為下面的代碼,通過 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);} } 復制代碼打開瀏覽器的開發(fā)者工具,可以在 performance 里面看到,頁面性能有了驚人的提升:
為了進一步提升頁面的性能,我們給過渡的元素增加一個 will-change 屬性,該元素就會被提升到 合成層 用GPU單獨渲染,這樣頁面性能就會有更大的提升。
優(yōu)化懶加載(需考慮兼容性)
有一些頁面使用了懶加載,懶加載是通過綁定 scroll 事件一個回調事件,每一次調用一次回調事件,就會測量一次元素的位置,調用 getBoundingClientRect() 方法,從而計算出是否元素出現在了可視區(qū)。
// 懶加載庫中的代碼,判斷是否進入了可視區(qū) const isInView = (el, threshold) => {const {top, height} = el.getBoundingClientRect()return top < clientHeight + threshold && top + height > -threshold }復制代碼scroll 造成頁面性能下降
scroll 事件會被重復的觸發(fā),每觸發(fā)一次就要測量一次元素的尺寸和位置。盡管對 scroll 的事件進行了節(jié)流的處理,但在低端安卓機上仍然會出現滑動不流暢的現象。
優(yōu)化的思路是通過新增的api—— IntersectionObserver 來獲取元素是否進入了可視區(qū)。
使用intersection observer
intersection observer api 可以去測量某一個dom節(jié)點和其他節(jié)點,甚至是viewport的距離。
這個是實驗性的api,你應該查閱https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility查看其兼容性
在過去,檢測一個元素是否在可視區(qū)內,或者兩個元素之間的距離如何,是一個非常艱巨的任務。 但獲取這些信息是非常必要的:
在過去,我們需要不斷的調用 Element.getBoundingClientRect() 方法去獲取到我們想拿到的信息,然而這些代碼會造成性能問題。
intersection observer api 可以注冊回調函數,當我們的目標元素,進入指定區(qū)域(比如說viewport,或者其他的元素)時,回調函數會被觸發(fā);
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].targetif (arr[0].isIntersecting) {let imgsrc = target.dataset.imgsrcif (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)復制代碼然后測試一下,發(fā)現可以正常使用:
比較性能
傳統(tǒng)的懶加載 lazy-loder 的頁面性能如下:
在12秒內,存在紅顏色的掉幀現象,一些地方的幀率偏低(在devtool里面是fps的綠色小山較高的地方),用于 scripting 階段的總共有600多ms.
使用intersetctionObserver之后的懶加載性能如下:
在12秒內,幀率比較平穩(wěn),用于 scripting 階段的時間只有60多ms了。參考鏈接:
總結
以上是生活随笔為你收集整理的深入理解浏览器解析和执行过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简单实现迷你Vue框架
- 下一篇: 【译】如何停止使用console.log