渲染流程
一個(gè)頁面呈現(xiàn)通過瀏覽器呈現(xiàn)到出來女坑,會(huì)經(jīng)過以下步驟
- 解析頁面內(nèi)容
- 構(gòu)建DOM樹
- 構(gòu)建CSSOM樹
- 合并DOM樹和CSSOM樹填具,生成Render-Tree(渲染樹)
- 基于當(dāng)前的viewport計(jì)算出每個(gè)元素的位置和尺寸等幾何信息(Layout)
- 將元素信息轉(zhuǎn)換成屏幕上的像素(Painting)
- 顯示(Display)
構(gòu)建DOM樹
DOM樹是對html文檔結(jié)構(gòu)的描述,它存儲(chǔ)了html文檔標(biāo)簽的屬性和關(guān)系匆骗,每一個(gè)html標(biāo)簽都會(huì)存在一個(gè)對應(yīng)的DOM元素劳景,元素的嵌套層級(jí)決定了他們的上下層關(guān)系,最終所有DOM元素會(huì)構(gòu)成一顆DOM樹碉就。
以下是一個(gè)簡單的html頁面:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
它的構(gòu)造過程如下:
- 編碼:瀏覽器處理接收到的HTML原始字節(jié)數(shù)據(jù)盟广,根據(jù)指定的編碼將字節(jié)轉(zhuǎn)換成字符(Bytes —> Characters)。
- 提取標(biāo)簽:瀏覽器根據(jù)W3C HTML5 標(biāo)準(zhǔn)從第一步中得到的字符串中提取出各種html標(biāo)簽瓮钥,例如筋量,“<html>”、“<body>”碉熄,以及其他尖括號(hào)內(nèi)的字符串桨武。每個(gè)令牌都具有特殊含義和一組規(guī)則(Characters —> Tokens)。
- 詞法分析: 根據(jù)第二部中得到的標(biāo)簽锈津,根據(jù)html元素的語言玻募、規(guī)則、屬性一姿,將其轉(zhuǎn)換成一個(gè)個(gè)元素對象七咧。
- DOM構(gòu)建: 步驟三中創(chuàng)建的每一個(gè)對象都會(huì)鏈接在一個(gè)數(shù)據(jù)結(jié)構(gòu)內(nèi),該結(jié)構(gòu)會(huì)捕獲原始標(biāo)記中定義的父項(xiàng)-子項(xiàng)關(guān)系叮叹,如:HTML 對象是 body 對象的父項(xiàng)艾栋,body 是 paragraph 對象的父項(xiàng),依此類推蛉顽。利用鏈接結(jié)構(gòu)蝗砾,構(gòu)建出DOM樹
CSSOM樹
CSS定義了html文檔可以應(yīng)用的樣式表,CSSOM樹存儲(chǔ)了CSS的對象模型結(jié)構(gòu)携冤。在瀏覽器為頁面的html元素計(jì)算樣式時(shí)悼粮,可以通過遍歷CSSOM來查找匹配的樣式表。
以以下CSS為例:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
CSSOM樹產(chǎn)生的過程與DOM樹的構(gòu)建過程類似曾棕,具體如下:
瀏覽器會(huì)根據(jù)CSS的語法標(biāo)準(zhǔn)扣猫,解析CSS文件,并生成相應(yīng)的對象翘地,最后構(gòu)成CSSOM樹申尤,上面的CSS最終構(gòu)成的CSSOM樹如下:
body是所有顯示元素的根節(jié)點(diǎn),它定義了font-size:16px
衙耕,由于所有的元素都是body的子項(xiàng)昧穿,所以他們會(huì)繼承body的樣式(CSS的級(jí)聯(lián)規(guī)則)。
注意:
以上樹并非完整的 CSSOM 樹橙喘,它只顯示了我們決定在樣式表中替換的樣式时鸵。每個(gè)瀏覽器都提供一組默認(rèn)樣式(也稱為“User Agent 樣式”),即我們不提供任何自定義樣式時(shí)所看到的樣式厅瞎,我們的樣式只是替換這些默認(rèn)樣式(例如默認(rèn) IE 樣式)饰潜。
構(gòu)建渲染樹
渲染樹通過合并DOM和CSSOM得到,它只包含渲染網(wǎng)頁所需的(需要顯示的)節(jié)點(diǎn)磁奖,以上面的DOM樹和CSSOM樹為例囊拜,他們合并后如下:
具體步驟如下:
-
從 DOM 樹的根節(jié)點(diǎn)開始遍歷每個(gè)可見節(jié)點(diǎn)。
- 某些節(jié)點(diǎn)不可見(例如腳本標(biāo)記比搭、元標(biāo)記等)冠跷,因?yàn)樗鼈儾粫?huì)體現(xiàn)在渲染輸出中,所以會(huì)被忽略身诺。
-
對于每個(gè)可見節(jié)點(diǎn)蜜托,為其找到適配的 CSSOM 規(guī)則并應(yīng)用它們。
- 如果一個(gè)DOM節(jié)點(diǎn)通過 CSS進(jìn)行了隱藏霉赡,那么它在渲染樹中也會(huì)被忽略橄务,例如,上例中的p標(biāo)簽下的span 節(jié)點(diǎn)就不會(huì)出現(xiàn)在渲染樹中穴亏,因?yàn)橛幸粋€(gè)顯式規(guī)則在該節(jié)點(diǎn)上設(shè)置了“display: none”屬性(visible屬性不會(huì))
Note:
visibility: hidden
與display: none
是不一樣的蜂挪。前者隱藏元素重挑,但元素仍占據(jù)著布局空間(即將其渲染成一個(gè)空框),而后者 (display: none
) 將元素從渲染樹中完全移除棠涮,元素既不可見谬哀,也不是布局的組成部分。 生成渲染樹严肪,得到最終要顯示的所有元素和它的樣式史煎。
注意:
只有同時(shí)具有 DOM 和 CSSOM 才能開始構(gòu)建渲染樹
布局
布局階段會(huì)根據(jù)渲染樹中得到的元素及其樣式信息去計(jì)算元素在當(dāng)前設(shè)備上的具體的大小、位置驳糯,所有相對測量值都轉(zhuǎn)換為屏幕上的絕對像素篇梭。
以下面的代碼為例:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
head中的meta標(biāo)簽聲明了當(dāng)前頁面的寬度應(yīng)該與設(shè)備寬度相等width=device-width
,不進(jìn)行縮放initial-scal=1
. Body 下的第一個(gè)div采用默認(rèn)布局酝枢,它的寬度為其父容器的50%恬偷,它下面的div同樣采用默認(rèn)布局,寬度又為它父親節(jié)點(diǎn)的50%隧枫。
假如當(dāng)前設(shè)備的寬度為320px喉磁,則第一個(gè)div的寬度就是160px,第二個(gè)div的寬度就是80px官脓,最終效果如下
繪制
在這個(gè)階段协怒,瀏覽器會(huì)根據(jù)在Layout階段計(jì)算出的所有節(jié)點(diǎn)的幾何信息(大小、位置)卑笨,將其繪制到屏幕孕暇。
至此,頁面已經(jīng)可以在屏幕顯示出來赤兴。
DOM & CSSOM & JS
瀏覽器將不會(huì)渲染任何已處理的內(nèi)容妖滔,直至 CSSOM 構(gòu)建完畢
同時(shí)具有 DOM 和 CSSOM 才能構(gòu)建渲染樹
當(dāng)瀏覽器遇到一個(gè) script 標(biāo)記時(shí),DOM 構(gòu)建將暫停桶良,直至腳本完成執(zhí)行
如果瀏覽器尚未完成 CSSOM 的下載和構(gòu)建座舍,此時(shí)有Javscript腳本需要運(yùn)行,瀏覽器將延遲腳本執(zhí)行和 DOM 構(gòu)建陨帆,直至其完成 CSSOM 的下載和構(gòu)建
圖像不會(huì)阻止頁面的首次渲染
下面是一個(gè)具體的例子:
1# <!DOCTYPE html>
2# <html>
3# <head>
4# <meta name="viewport" content="width=device-width,initial-scale=1">
5# <link href="style.css" rel="stylesheet">
6# <title>Critical Path: Script External</title>
7# </head>
8# <body>
9# <p>Hello <span>web performance</span> students!</p>
10# <div><img src="awesome-photo.jpg"></div>
11# <script src="app.js"></script>
12# </body>
13# </html>
在這個(gè)例子中曲秉,整個(gè)頁面的加載流程如下:
- 瀏覽器解析html文檔(編碼—>提取標(biāo)簽—>詞法分析)
- 然后開始構(gòu)建DOM
- 第5行,發(fā)現(xiàn)link標(biāo)簽疲牵,開始加載CSS文件并創(chuàng)建CSSOM
- 第11行承二,發(fā)現(xiàn)script標(biāo)簽,開始加載外部腳本纲爸,阻塞DOM的構(gòu)建
- 腳本加載完畢亥鸠,開始執(zhí)行腳本,此時(shí)如果
- CSSOM已經(jīng)構(gòu)建完畢识啦,則直接執(zhí)行腳本
- CSSOM并沒有構(gòu)建完畢负蚊,則等待CSSOM構(gòu)建完畢神妹,然后執(zhí)行腳本
- 腳本執(zhí)行完畢,繼續(xù)DOM構(gòu)建
- 利用構(gòu)建好的DOM+CSSOM盖桥,穿件渲染樹
- 繪制并顯示
渲染過程性能優(yōu)化
優(yōu)化關(guān)鍵渲染路徑
關(guān)鍵渲染路徑指的是 HTML 標(biāo)記灾螃、CSS 和 JavaScript,圖像不會(huì)阻止頁面的首次渲染揩徊。通過優(yōu)化關(guān)鍵渲染路徑,可以縮短首次渲染頁面的時(shí)間嵌赠。
可以通過以下維度進(jìn)行優(yōu)化:
-
減少資源數(shù)量塑荒、字節(jié)大小。
如通過壓縮css和js文件來減小他們的字節(jié)數(shù)姜挺,通過合并文件來減少http請求的數(shù)量齿税,以此來提高資源加載的速度
-
盡早加載CSS資源
由于CSSOM會(huì)阻塞渲染,只有DOM和CSSOM加載完畢后渲染才開始執(zhí)行炊豪,因此應(yīng)該盡量早的加載CSS資源
-
利用media屬性聲明CSS的使用場景
<link href="style.css" rel="stylesheet" media="orientation:landscape">
上面的例子中凌箕,聲明了media的值為
orientation:portrait
,則這個(gè)style.css文件只有在橫屏狀況下才會(huì)使用,其他狀況都不會(huì)阻塞渲染词渤。關(guān)于media的更多資料牵舱,可以查看這里
-
將腳本標(biāo)記為異步
默認(rèn)情況下,所有 JavaScript 都會(huì)阻止解析器缺虐,向 script 標(biāo)記添加異步關(guān)鍵字可以指示瀏覽器在等待腳本加載期間不阻止 DOM 構(gòu)建芜壁,如下
<script src="app.js" async></script>
提升交互性能
交互或者動(dòng)畫都會(huì)出發(fā)頁面的重繪,他會(huì)影響用戶的操作流暢性體驗(yàn)高氮。當(dāng)前主流的設(shè)備的幀率都是60fps慧妄,因此網(wǎng)頁應(yīng)用渲染完一幀的速率就應(yīng)該低于16ms這樣才不會(huì)掉幀。在這16ms中剪芍,瀏覽器需要完成以下工作:
執(zhí)行JavaScript腳本(增刪改dom或style) -> 計(jì)算節(jié)點(diǎn)樣式 -> 計(jì)算節(jié)點(diǎn)的集合信息(位置塞淹、大小) -> 像素填充 -> 圖層合并?
Javascript和CSS是由開發(fā)者提供的,其他步驟都完全由瀏覽器控制罪裹,因此饱普,我們的腳本的處理時(shí)間應(yīng)該低于16ms才能保證頁面的重繪在16ms內(nèi)完成,一般來說坊谁,留給JavaScript執(zhí)行的時(shí)間大概在10ms左右费彼。
使用 requestAnimationFrame
來實(shí)現(xiàn)視覺變化
JavaScript執(zhí)行的最佳時(shí)機(jī)是在屏幕開始繪制新一幀的開頭。 requestAnimationFrame接受一個(gè)函數(shù)口芍,并保證該函數(shù)在新的一幀的開頭被調(diào)用箍铲,我們可以使用這個(gè)方法來處理動(dòng)畫。下面是一個(gè)小的示例:
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function move(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(move);
}
}
window.requestAnimationFrame(move);
不推薦使用 setTimeout
或 setInterval
來執(zhí)行動(dòng)畫之類的視覺變化的原因在于鬓椭,回調(diào)將在幀中的某個(gè)時(shí)點(diǎn)運(yùn)行颠猴,可能剛好在末尾关划,而這可能經(jīng)常會(huì)使我們丟失幀,導(dǎo)致卡頓翘瓮。
減少/避免布局操作
當(dāng)元素的“l(fā)ayout”屬性發(fā)生了改變贮折,也就是改變了元素的幾何屬性(例如寬度、高度资盅、左側(cè)或頂部位置等)调榄,那么瀏覽器將必須檢查所有其他元素,然后“自動(dòng)重排”頁面呵扛。任何受影響的部分都需要重新繪制每庆,而且最終繪制的元素需進(jìn)行合成。
常見的幾何屬性有:
bottom, direction,display,float,font-size,font-weight,height,left,margin,padding,position,right,top,width...
可以通過https://csstriggers.com/查看更多信息今穿。
避免強(qiáng)制同步布局
正常的頁面更新流程是JavaScript 運(yùn)行 -> 計(jì)算樣式 -> 布局, 但是在以下場景缤灵,JavaScript會(huì)強(qiáng)制瀏覽器提前進(jìn)行布局:
var box = document.getElementById("example-dom");
box.classList.add('fiori'); //修改樣式
console.log(box.offsetHeight); //讀取高度
在給box節(jié)點(diǎn)添加新的樣式ful-screen
之前,自上一幀的所有舊布局值是已知的蓝晒,瀏覽器為了優(yōu)化性能腮出,會(huì)將更新操作暫緩到隊(duì)列批量處理;但是芝薇,程序立馬請求查看節(jié)點(diǎn)的高度box.offsetHeight
胚嘲,為了獲取準(zhǔn)確的數(shù)據(jù),瀏覽器必須立刻應(yīng)用樣式更改剩燥,然后運(yùn)行布局慢逾,這是不必要的,如果有大量的這種操作(處理動(dòng)畫時(shí)灭红,往往會(huì)不停的修改樣式侣滩,應(yīng)該避免寫與讀操作相間觸發(fā)),將會(huì)帶來巨大的開銷变擒。
正確的做法是應(yīng)該先獲取高度君珠,然后再進(jìn)行樣式的更改
var box = document.getElementById("example-dom");
console.log(box.offsetHeight); //讀取高度
box.classList.add('fiori'); //修改樣式
反例:
for(var i=0;i<p.length;i++){
p[i].style.width = d.offsetWidth + 'px'; //獲取d的寬度并賦值給p[i]
}
正例:
var w = d.offsetWidth; // 提前獲取好d的寬度
for(var i=0;i<p.length;i++){
p[i].style.width = w + 'px'; //通過變量w給p[i]賦值,循環(huán)中只有寫操作娇斑,不會(huì)強(qiáng)制同步布局
}
后臺(tái)運(yùn)行復(fù)雜/耗時(shí)邏輯
JavaScript 在瀏覽器的主線程上運(yùn)行策添,恰好與樣式計(jì)算、布局以及許多情況下的繪制一起運(yùn)行毫缆。如果 JavaScript 運(yùn)行時(shí)間過長唯竹,就會(huì)阻塞這些其他工作,可能導(dǎo)致幀丟失苦丁。對于一些計(jì)算密集的操作浸颓,建議使用web worker進(jìn)行處理,不要占用瀏覽器的主進(jìn)程
減少繪制區(qū)域
繪制并非總是繪制到內(nèi)存中的單個(gè)圖像。事實(shí)上产上,在必要時(shí)瀏覽器可以繪制到多個(gè)圖像或合成器層棵磷,類似于Photoshop圖層的概念,我們可以創(chuàng)建不同的圖層繪制圖像晋涣,最后將他們合并仪媒。
利用這個(gè)特性,可以將經(jīng)常需要重繪的部分單獨(dú)放在一個(gè)圖層谢鹊,避免整個(gè)頁面收到影響算吩。
使用 will-change CSS 屬性可以為元素創(chuàng)建一個(gè)新的圖層, will-change
為web開發(fā)者提供了一種告知瀏覽器該元素會(huì)有哪些變化的方法撇贺,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)的優(yōu)化準(zhǔn)備工作赌莺。
.moving-element {
will-change: transform;
}
注意:
不要?jiǎng)?chuàng)建太多圖層,因?yàn)槊繉佣夹枰獌?nèi)存和管理開銷松嘶。
其他
- 不要使用過于復(fù)雜的css選擇器
- 精簡DOM節(jié)點(diǎn)數(shù)量
工具
性能監(jiān)控工具
- Chrome DevTool(強(qiáng)力推薦,以后有機(jī)會(huì)會(huì)單獨(dú)介紹)
- Lighthouse