在咱們公司的業務場景中,有很大一部分用戶是使用老款安卓機瀏覽頁面,這些老款安卓機性能較差,若是優化不足,頁面的卡頓現象會更加明顯,此時頁面性能優化的重要性就凸顯出現。優化頁面的性能,須要對瀏覽器的渲染過程有深刻的瞭解,針對瀏覽器的每一步環節進行優化。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 tree
和dom tree
結合在一塊兒生成了render tree
。java
render tree
構建好以後,渲染引擎隨後會經歷一個layout
的階段: 計算出每個節點應該出如今屏幕上的確切座標。node
以後的階段被稱爲paiting
階段,渲染引擎會遍歷render tree
, 而後由用戶界面後端層將每個節點繪製出來。git
最後一個階段是 composite
階段,這個階段是合併圖層。程序員
瀏覽器是一個極其複雜龐大的軟件。常見的瀏覽器有chrome, firefox。firefox是徹底開源,Chrome不開源,但Chromium項目是部分開源。web
Chromium和Chrome之間的關係相似於嚐鮮版和正式版的關係,Chromium會有不少新的不穩定的特性,待成熟穩定後會應用到Chrome。
瀏覽器功能有不少,包括網絡、資源管理、網頁瀏覽、多頁面管理、插件和擴展、書籤管理、歷史記錄管理、設置管理、下載管理、帳戶和同步、安全機制、隱私管理、外觀主題、開發者工具等。
所以瀏覽器內部被劃分爲不一樣的模塊。其中和頁面渲染相關的,是下圖中虛線框的部分渲染引擎。
渲染引擎的做用是將頁面轉變成可視化的圖像結果。
目前,主流的渲染引擎包括Trident、Gecko和WebKit,它們分別是IE、火狐和Chrome的內核(2013年,Google宣佈了Blink內核,它實際上是從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 被稱爲詞法分析的過程,有的文章也稱爲 tokenization,其實就是把輸入的內容分爲不一樣的tokens(標記),tokens是最小的組成部分,tokens就像是人類語言中的一堆詞彙。好比說,咱們對一句英文進行lexical analysis——「The quick brown fox jumps」,咱們能夠拿到如下的token:
用來作lexical analysis的工具,被稱爲**lexer
**, 它負責把輸入的內容拆分爲不一樣的tokens。不一樣的瀏覽器內核會選擇不一樣的lexer , 好比說webkit 是使用Flex (Fast Lexer)做爲lexer。
syntax analysis是應用語言句法中的規則, 簡單來講,就是判斷一串tokens組成的句子是否是正確的。
若是我說:「我吃飯工做完了」, 這句話是不符合syntax analysis的,雖然裏面的每個token都是正確的,可是不符合語法規範。須要注意的是,符合語法正確 的句子不必定是符合語義正確的。好比說,「一個綠色的夢想沉沉的睡去了」,從語法的角度來說,形容詞 + 主語 + 副詞 + 動詞沒有問題,可是語義上倒是什麼鬼。
負責syntax analysis
工做的是**parser
**,解析是一個不斷往返的過程。
以下圖所示,parser
向lexer
要一個新的token
,lexer
會返回一個token
, parser
拿到token
以後,會嘗試將這個token
與某條語法規則進行匹配。
若是該token
匹配上了語法規則,parser
會將一個對應的節點添加到 parse tree (解析樹,若是是html就是dom tree,若是是css就是 cssom tree)中,而後繼續問parser要下一個node。
固然,也有可能該tokens
沒有匹配上語法規則,parser
會將tokens
暫時保存,而後繼續問lexer
要tokens
, 直至找到可與全部內部存儲的標記匹配的規則。若是找不到任何匹配規則,parser
就會引起一個異常。這意味着文檔無效,包含語法錯誤。
syntax analysis
的輸出結果是parse tree, parse tree 的結構表示了句法結構。好比說咱們輸入"John hit the ball"做爲一句話,那麼 syntax analysis
的結果就是:
一旦咱們拿到了parse tree
, 還有最後一步工做沒有作,那就是:translation
,還有一些博客將這個過程成爲 compilation / transpilation / interpretation
上面提到了lexer
和 parser
這兩個用於解析工具,咱們一般不會本身寫,而是用現有的工具去生成。咱們須要提供一個語言的 lexicon
和 syntaxes
,才能夠生成相應的 lexer
和 parser
。
webkit 使用的 lexer 和 parser 是 Flex 和 Bison 。
flex
和css
的 flex
佈局沒有關係,是 fast-lexer
的簡寫,用來生成 lexer
。 它須要一個lexicon
,這個lexicon
是用一堆正則表達式來定義的 。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
一般是被一個叫無上下文語法所定義,關於無上下文語法能夠點擊這個連接,反正只須要知道,無上下文語法要比常規的語法更復雜就行了。
非科班出身的前端可能不瞭解 BNF 範式(說的就是我 --),它是一種形式化符號來描述給定語言的語法。
它的內容大體爲:
下面是用BNF來定義的Java語言中的For語句的實例。
FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ]
")" statement
複製代碼
BNF 的誕生仍是挺有意思的一件事情, 有了BNF纔有了真正意義上的計算機語言。巴科斯範式直到今天,仍然是個迷,巴科斯是如何想到的
咱們如今對解析過程有了一個大體的瞭解,總結成一張圖就是這樣:
對解析(parse)有了初步的瞭解以後,咱們看一下HTML的解析過程。
HTML是不規範的,咱們在寫html的代碼時候,好比說漏了一個閉合標籤,瀏覽器也能夠正常渲染沒有問題的。這是一把雙刃劍,咱們能夠很容易的編寫html, 可是卻給html的解析帶來很多的麻煩,更詳細的信息能夠點擊:連接
Html 的 lexicon 主要包括6個部分:
當一個html文檔被lexer 處理的時候,lexer 從文檔中一個字符一個字符的讀出來,而且使用 finite-state machine 來判斷一個完整的token是否已經被完整的收到了。
這裏就是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 的容錯性是很是高的,這樣是有代價的,這增長了解析的困難,讓詞法解析解析更加困難。
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的解析了。
和html 解析相比,css 的解析就簡單不少了。
關於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 定義:
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*;
複製代碼
CSS解析獲得的parse tree 通過加工以後,就獲得了CSSOM Tree。 CSSOM 被稱爲「css 對象模型」。
CSSOM Tree 對外定義接口,能夠經過js去獲取和修改其中的內容。開發者能夠經過document.styleSheets
的接口獲取到當前頁面中全部的css樣式表。
那麼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對象。
CSSStylelSheet對象又長什麼樣子呢?以下圖所示:
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的前邊,有兩個緣由:
當瀏覽器忙着構建DOM Tree和 CSSOM Tree的時候, 瀏覽器同時將二者結合生成Render Tree。也就是說,瀏覽器構建DOM Tree和 CSSOM Tree ,和結合生成Render Tree,這兩個是同時進行的。
Levi Weintraub(webkit 的做者之一)在一次分享(分享的視頻點這裏,分享的ppt點這裏)中開玩笑說,準確的來講,咱們你們提的Render Tree應該是Render Forest (森林)。由於事實上,存在多條Tree:
這裏作一點說明。
有不少其餘的文章中提到了 Render Tree,其中的每個構成的節點都是 Render Obejcts, 所以其餘文章中的 Render Tree 概念,在本文中等同於 Render Object Tree ( Levi Weintraub 和 Webkit core 的叫法都是Render Object Tree, 其餘文章中 Render Tree的本義也應是 Render Object Tree)。
DOM Tree 和 Render Object Tree 之間的關係是什麼樣的?
Render Object Tree 並不嚴格等於Dom Tree,先看一張DOM Tree 和 Render Object Tree的直觀的對比圖:
上面左側DOM tree的節點對應右側Render Object Tree上的節點。細心的你會注意到,上圖左側的DOM Tree中的HTMLDivElement 會變成RenderBlock, HTMLSpanElement 會變成RenderInline,也就是說,DOM節點對應的 render object 節點並不同。
DOM節點對應的 render object 節點並不同分這幾種狀況:
display : none
的DOM 節點沒有對應的 Render Object Tree 的節點
這裏的display:none
屬性,有多是咱們在CSS裏面設置的,也有多是瀏覽器默認的添加的屬性。好比說下面的元素就會有默認的display:none
的屬性。
<head>
<meta>
<link>
<script>
下面的各個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:
脫離文檔流的狀況,要麼是float
, 要麼是position: absolute / fixed
。
好比說對於下面的結構:
<body>
<div>
<p>Lorem ipsum</p>
</div>
</body>
複製代碼
它的 DOM tree
和 Render Tree
以下圖所示:
若是增長脫離文檔流的樣式,以下:
p {
position: absolute;
}
複製代碼
狀況就會變成下面這樣:
<p>
節點對應的 Render Tree 的節點,從父節點脫離出來,掛到了頂部的RenderView
節點下面。
爲何脫離了文檔流的節點,在 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的屬性
咱們在CSS中接觸過文檔流的概念,文檔流中的元素分爲塊狀元素和行內元素,好比說div
是塊狀元素,span
是行內元素。塊狀元素和行內元素在文檔流中的表現不一樣,就是在這裏決定的。
Render Object 的節點類型分爲下面幾種:
RenderBlock
display: block
的DOM節點對應的render object節點類型爲RenderBlock
RenderInline
display:inline
的DOM節點對應的render object節點類型爲RenderInline
RenderReplaced
可能咱們以前據說過「替換元素」 的概念,好比說常見的「替換元素」有下面:
<iframe>
<video>
<embed>
<img>
爲啥被稱爲「替換元素」,是由於他們的內容會被一個獨立於HTML/CSS上下文的外部資源所替代。
「替代元素」 的DOM節點對應的render object 節點類型爲RenderReplaced
RenderTable
<table>
元素的DOM節點對應的render object 節點類型爲 RenderTable
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時,渲染引擎幫咱們作了不少事情。
render object tree 遵照2個準則:
anonymounse renderers(匿名的render object 節點)就是用於處理不遵照這兩種規則的代碼的,若是出現不符合這兩個準則的狀況,好比說下面:
<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>
複製代碼
<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時就須要更多的工做去糾正,從而形成頁面性能的降低。
Gecko 和 WebKit 採用了不一樣的方案來構建 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的信息,才能夠被繪製在屏幕上,這就須要樣式計算這一過程。
而樣式計算須要兩部分「原材料」:
DOM Tree在HTML解析以後就能夠拿到了,一堆樣式規則能夠來自下面:
那麼樣式規則是如何構成的呢?
樣式計算存在如下三個難點:
下面咱們介紹這個三個難點是如何解決的。
某一個DOM節點上可能有多個規則,好比下下面:
div p {
color: goldenrod;
}
p {
color: red;
}
複製代碼
那麼這個DOM節點究竟用的是哪一個規則?
規則的權重是:先看 order , 而後再算specificity, 最後再看哪一個規則靠的更近。
order的權重從高到底:
! important 聲明
(瀏覽器可讓用戶導入自定的樣式)Specificity是一個相加起來的值
#foo .bar > [name="baz"]::first-line {} /* Specificity: 0 1 2 1 */
複製代碼
第一位的數值(a)
是否有DOM節點上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數據太多,不是說咱們寫的css樣式太多,而是Render Object Tree每個節點上都須要存儲所有的CSS樣式,那些沒有被指定的樣式,其值爲繼承父節點的樣式,或者是瀏覽器的默認樣式,或者乾脆是個空值。
webkit 和 gecko 採用了不一樣的解決方案。
WebKit 的解決方案是,節點們會引用RenderStyle對象。這些對象在如下狀況下能夠由不一樣的DOM節點共享,從而節省空間和提升性能。
div + p
, div ~ p
, :last-child
, :first-child
, :nth-child()
, :nth-of-type()
Gecko 採用了一種 style struct sharing 的機制。有一些css屬性能夠聚合在一塊兒,好比說font-size, font-family, color 等等,瀏覽器就把這些能夠被劃分爲一組的屬性,單獨的保存到一個對象裏面,這個對象被稱爲 style struct。以下表所示:
上圖中的,computed style 裏就不用存儲CSS所有的200多個屬性,而是保存着對這些 style struct對象的引用。
這樣一來,一些具備相同屬性的DOM 節點就能夠引用相同的 style struct
, 不只如此,由於子節點有一些屬性會繼承父節點,那麼保存這些屬性的 style struct
就會被父節點和子節點所共享。
對於每個DOM節點,css引擎須要去遍歷全部的css規則看是否匹配。對於大部分的DOM節點,css規則的匹配並不會發生改動。
好比說,用戶把鼠標hover到一個父元素上面,這個父元素的css規則匹配是發生了變化,咱們不只僅要從新計算這個父元素的樣式,還須要從新計算這個父元素的子元素的樣式(由於要處理繼承的樣式),可是能匹配這些子元素的樣式規則,是不會變的。
若是咱們能記下來,有哪些selector能夠匹配到就行了。
爲了優化這一點,CSS 引擎在進行 selector 匹配時,會根據權重的順序把他們排成一串,而後把這一串加到右邊的 CSS rule tree 上面。
CSS引擎但願右邊CSS rule tree 的分支數越少越好,所以會將新加入的一串儘可能的合併到已有的分支,因此上面的過程會是下面這樣的:
而後遍歷每個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 都對此進行了優化,在遍歷完一個節點以後,會把計算好的樣式放到緩存中,在遍歷下一個節點以前,會作一個判斷,看是否能夠複用緩存中的樣式。
這個判斷包括一下幾點:
兩個節點是否有相同的id, class
兩個節點是否有相同的style 屬性
兩個節點對應的父親節點是否共享一份計算好的樣式,那該兩個節點繼承的樣式也是相同的。
上面在構建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,而後記錄渲染數據以下:
其中花費了 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,而後記錄渲染數據以下:
能夠發現,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 工具來自動化的完成。
在上一節咱們提到了 render object tree, render 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 :
全局layout
render tree 的根節點自身的layout方法被調用,而後整個render tree 被更新。
局部layout
只是區域性的更新,只適用於某個分支的改動不會影響到周圍的分支。
目前局部layout只會在 text 更新的時候使用
在layout 階段,採用一種稱爲 Dirty Bits 的機制去判斷一個節點是否須要layout。當一個新的節點被插到tree中時,它不只僅「弄髒「了它自身,還「弄髒「了相關的父節點(the relevant ancestor chain,下面會介紹)。有沒有被「弄髒」是經過設置bits (set bits)來標識的。
bool needsLayout() const { return m_needsLayout || m_normalChildNeedsLayout || m_posChildNeedsLayout; }
複製代碼
上面 needsLayout 爲 true 有三種狀況:
selfNeedsLayout
Rederer 自身是 「髒」的。當一個 rederer 自身被設置爲「髒」的,它相關的父親節點也會被設置一個標識來指出它們有一個「髒」的子節點
posChildNeedsLayout
設置了postion不爲static的子節點被弄髒了
normalChildNeedsLayout
在文檔流中的子節點被弄髒了
上面之因此要區分子節點是否在文檔流中,是爲了layout過程的優化。
上面提到了相關父節點(the relevant ancestor chain),那麼到底是如何判斷哪一個節點是 **相關父節點 **?
答案就是經過 containing block.
Container Block(包含塊) 身份有兩個
子節點的相關的父節點
子節點的相對座標系
子節點都有 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) 的位置。下面是圖示:
黑色的框表明的是 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 ,原文以下:
而webkit core 認爲css postion 的值爲 fixed的renderer節點的containing block是RenderView。RenderView並不會表現的和viewport同樣,可是RenderView會根據頁面滾動的距離算出css postion 的值爲 fixed的renderer節點的位置。這是由於單獨爲viewport 生成一個renderer 節點並不簡單。原文以下:
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。
盒子模型相關的屬性
width
height
padding
margin
border
display
定位屬性和浮動
節點內部的文字結構
上面只是一部分,更所有的能夠點擊 csstriggers 來查閱;
csstrigger 裏面須要注意的有幾點。
opacity的改動,在blink內核和Gecko內核上不會觸發layout 和 repaint
transform的改動,在blink內核和Gecko內核上不會觸發layout 和 repaint
visibility 的改動,在Gecko 內核上不會觸發 layout repaint, 和 composite
幾乎任何測量元素的寬度,高度,和位置的方法都會不可避免的觸發reflow, 包括可是不限於:
雖然切換className 也會形成性能上的影響,可是次數上減小了。
好比說必定要修改這個dom節點100次,那麼先把dom的display設置爲 none ( 僅僅會觸發一次迴流 )
老的佈局模型以相對/絕對/浮動的方式將元素定位到屏幕上 Floxbox佈局模型用流式佈局的方式將元素定位到屏幕上,flex性能更好。
使用table佈局哪怕一個很小的改動都會形成從新佈局
layout根據區域來劃分的,分爲全局性layout, 和局部的layout。好比說修改根字體的大小,會觸發全局性layout。
全局性layout是同步的,會馬上立刻被執行,而局部性的layout是異步的,分批次的。瀏覽器會嘗試合併屢次局部性的layout爲一次,而後異步的執行一次,從而提升效率。
可是js一些操做會觸發強制性的同步佈局,從而影響頁面性能,好比說讀取 offsetHeight、offsetWidth 值的時候。
第三個階段是paint 階段
使用transform不會觸發layout , 只會觸發paint。
若是你想頁面中作一些比較炫酷的效果,相信我,transform能夠知足你的需求。
// 位置的變換
transform: translate(1px,2px)
// 大小的變換
transform: scale(1.2)
複製代碼
由於 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 樣式會形成頁面性能的問題呢?
更多的內容請參考 鏈接
上面幾個階段能夠用下面一張圖來表示:
DOM 樹每一個 Node 節點都有一個對應的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的內容。
有相同座標的 LayoutObjects ,在同一個PaintLayer內。 根據建立PaintLayer 的緣由不一樣,能夠將其分爲常見的 3 類:
某些特殊的paintLayer會被當成合成層,合成層擁有單獨的 GraphicsLayer,而其餘不是合成層的paintLayer,則和其第一個擁有GraphicsLayer 父層公用一個。
每一個 GraphicsLayer 都有一個GraphicsContext,GraphicsContext 會負責輸出該層的位圖,位圖是存儲在共享內存中,做爲紋理上傳到 GPU 中,最後由 GPU 將多個位圖進行合成,而後 draw 到屏幕上,此時,咱們的頁面也就展示到了屏幕上。
渲染層提高爲合成層的緣由有一下幾種:
爲啥overlap 重疊也會形成提高合成層渲染? 圖層之間有重疊關係,須要按照順序合併圖層。
若是把一個頻繁修改的dom元素,抽出一個單獨的圖層,而後這個元素的layout, paint 階段都會在這個圖層進行,從而減小對其餘元素的影響。
使用 will-change
或者 transform3d
1. will-change: transform/opacity
2. transform3d(0,0,0,)
複製代碼
由於視頻中的每一幀都是在動的,因此視頻的區域,瀏覽器每一幀都須要重繪。因此瀏覽器會本身優化,把這個區域的給抽出一個單獨的圖層
須要注意的是,gif 圖片雖然也變化很頻繁,可是 img 標籤不會被單獨的提到一個複合層,因此咱們須要單獨的提到一個獨立獨立的圖層之類。
composite更詳盡的知識能夠了解下面這個博客: 《GPU Accelerated Compositing in Chrome》
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%
這個其實有兩個地方,第一是,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階段,
若是繼續優化,咱們經過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.
因此,對於這種動效,優先選擇 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查看其兼容性
在過去,檢測一個元素是否在可視區內,或者兩個元素之間的距離如何,是一個很是艱鉅的任務。 但獲取這些信息是很是必要的:
在過去,咱們須要不斷的調用 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的懶加載的庫。
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以後的懶加載性能以下:
scripting
階段的時間只有60多ms了。
參考連接: