導(dǎo)語:
文中多處引用牛人博客觀點夕春,重在分享知識和自己的一些見解~
一只酥,瀏覽器渲染頁面流程
1.瀏覽器解析html源碼哩罪,然后創(chuàng)建一個 DOM樹。
在DOM樹中锁摔,每一個HTML標(biāo)簽都有一個對應(yīng)的節(jié)點廓旬,并且每一個文本也都會有一個對應(yīng)的文本節(jié)點。DOM樹的根節(jié)點就是 documentElement谐腰,對應(yīng)的是html標(biāo)簽孕豹。
2.瀏覽器解析CSS代碼,計算出最終的樣式數(shù)據(jù)十气。
對CSS代碼中非法的語法她會直接忽略掉励背。解析CSS的時候會按照如下順序來定義優(yōu)先級:瀏覽器默認(rèn)設(shè)置 < 用戶設(shè)置 < 外鏈樣式 < 內(nèi)聯(lián)樣式 < html中的style。
3.構(gòu)建出DOM樹砸西,并且計算出樣式數(shù)據(jù)后叶眉,下一步就是構(gòu)建一個 渲染樹(rendering tree)。渲染樹和DOM樹有點像芹枷,但是是有區(qū)別的衅疙。DOM樹完全和html標(biāo)簽一一對應(yīng),但是渲染樹會忽略掉不需要渲染的元素鸳慈,比如head饱溢、display:none的元素等。而且一大段文本中的每一個行在渲染樹中都是獨立的一個節(jié)點走芋。渲染樹中的每一個節(jié)點都存儲有對應(yīng)的css屬性绩郎。
4.一旦渲染樹創(chuàng)建好了潘鲫,瀏覽器就可以根據(jù)渲染樹直接把頁面繪制到屏幕上。
重繪和重排(repaints and reflows)
每個頁面至少在初始化的時候會有一次重排操作肋杖。任何對渲染樹的修改都有可能會導(dǎo)致下面兩種操作:
1.重排就是渲染樹的一部分必須要更新 并且節(jié)點的尺寸發(fā)生了變化溉仑。這就會觸發(fā)重排操作。
2.重繪部分節(jié)點需要更新兽愤,但是沒有改變他的集合形狀彼念,比如改變了背景顏色,這就會觸發(fā)重繪浅萧。什么情況下會觸發(fā)重繪或重排
下面任何操作都會觸發(fā)重繪或者重排:
- 增加或刪除DOM節(jié)點設(shè)置 display: none;(重排并重繪) 或visibility: hidden(只有重排)
- 移動頁面中的元素
- 增加或者修改樣式用戶
- 改變窗口大小
- 滾動頁面等
減少重繪和重排
1.不要一個一個地單獨修改屬性逐沙,最好通過一個classname來定義這些修改
// badvar left = 10, top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// better el.className += " theclassname";
2.把對節(jié)點的大量修改操作放在頁面之外用 documentFragment來做修改clone 節(jié)點,在clone之后的節(jié)點中做修改洼畅,然后直接替換掉以前的節(jié)點通過 display: none 來隱藏節(jié)點(直接導(dǎo)致一次重排和重繪)吩案,做大量的修改,然后顯示節(jié)點(又一次重排和重繪)帝簇,總共只會有兩次重排徘郭。
3.不要頻繁獲取計算后的樣式。如果你需要使用計算后的樣式丧肴,最好暫存起來而不是直接從DOM上讀取残揉。
4.總的來說,總是考慮到渲染樹得存在芋浮,考慮到你的一次修改會導(dǎo)致多大的繪制操作抱环。比如絕對定位元素的動畫就不會影響其他大部分元素。
有個小段子:
1.用戶輸入網(wǎng)址(假設(shè)是個html頁面纸巷,并且是第一次訪問)镇草,瀏覽器向服務(wù)器發(fā)出請求,服務(wù)器返回html文件瘤旨;
2.瀏覽器開始載入html代碼梯啤,發(fā)現(xiàn)<head>標(biāo)簽內(nèi)有一個<link>標(biāo)簽引用外部CSS文件;
3.瀏覽器又發(fā)出CSS文件的請求存哲,服務(wù)器返回這個CSS文件因宇;
4.瀏覽器繼續(xù)載入html中<body>部分的代碼,并且CSS文件已經(jīng)拿到手了祟偷,可以開始渲染頁面了羽嫡;
5.瀏覽器在代碼中發(fā)現(xiàn)一個<img>標(biāo)簽引用了一張圖片,向服務(wù)器發(fā)出請求肩袍。此時瀏覽器不會等到圖片下載完,而是繼續(xù)渲染后面的代碼婚惫;
6.服務(wù)器返回圖片文件氛赐,由于圖片占用了一定面積魂爪,影響了后面段落的排布,因此瀏覽器需要回過頭來重新渲染這部分代碼艰管;
7.瀏覽器發(fā)現(xiàn)了一個包含一行Javascript代碼的<script>標(biāo)簽滓侍,趕快運行它;
8.Javascript腳本執(zhí)行了這條語句牲芋,它命令瀏覽器隱藏掉代碼中的某個<div> (style.display=”none”)撩笆。杯具啊,突然就少了這么一個元素缸浦,瀏覽器不得不重新渲染這部分代碼夕冲;
9.終于等到了</html>的到來,瀏覽器淚流滿面……
10.等等裂逐,還沒完歹鱼,用戶點了一下界面中的“換膚”按鈕,Javascript讓瀏覽器換了一下<link>標(biāo)簽的CSS路徑卜高;
11.瀏覽器召集了在座的各位<div><span><ul><li>們弥姻,“大伙兒收拾收拾行李,咱得重新來過……”掺涛,瀏覽器向服務(wù)器請求了新的CSS文件庭敦,重新渲染頁面。
有個小例子:
以下代碼在Chrome 51 Release版的Timeline
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" href="index.css">
</head>
<body>
</body>
<script>
document.querySelector('body').innerHTML = 'hello world';
</script>
</html>
頁面的加載過程為:
0.Event(beforeunload)
1.Send Request
2.Receive Response
3.Receive Data
4.Event(pagehide)
5.Event(visibilitychange)
6.Event(webkitvisibilitychange)
7.Event(unload)
8.Event(readystatechange)
9.Finish loading
10.Parse HTML
11.Update Layer Tree
12.Paint
13.firstPaint
14.firstContentfullPaint
15.composite Layers
16.Major GC(0 B collected)
17.Minor GC(123 KB collected)
注:
GC薪缆,即垃圾回收秧廉。
如果你還不太明白,這個是最后的解釋:
網(wǎng)頁渲染必須在很早的階段進行矮燎,可以早到頁面布局剛剛定型定血。因為樣式和腳本都會對網(wǎng)頁渲染產(chǎn)生關(guān)鍵性的影響。所以專業(yè)開發(fā)者必須了解一些技巧诞外,從而避免在實踐的過程中遇到性能問題澜沟。
渲染引擎的主要目的就是從一個網(wǎng)頁的URL開始,經(jīng)過一系列的復(fù)雜處理過程之后峡谊,變成一個可視化的結(jié)果茫虽,這一過程就是這里所說的頁面渲染的基本過程。
所謂的渲染既们,就是根據(jù)描述或者定義構(gòu)建數(shù)學(xué)模型濒析,通過模型生成圖像的過程。瀏覽器的渲染引擎就是能夠?qū)TML/CSS/JavaScript轉(zhuǎn)換成圖像結(jié)果的模塊啥纸,如下圖所示号杏,輸入是URL對應(yīng)的各種資源,輸出是可視化的圖像斯棒。從這里看盾致,非常的簡單和容易理解主经。
URL對應(yīng)的各種資源,輸出是可視化的圖像庭惜。從這里看罩驻,非常的簡單和容易理解。
渲染模塊
那么渲染引擎提供了哪些功能模塊來支持頁面渲染的呢护赊?下圖是一個渲染引擎所包含的基本功能和它們依賴的一些第三方庫惠遏。
引擎
從圖中大致可以看出,一個渲染引擎大致包括HTML解釋器骏啰,CSS解釋器节吮,布局和JavaScript引擎。下面依次來描述它們:
HTML解釋器:解釋HTML語言的解釋器器一,本質(zhì)是將HTML文本解釋成DOM(文檔對象模型)樹课锌。
CSS解釋器:解釋樣式表的解釋器,其作用是將DOM中的各個元素對象加上樣式信息祈秕,從而為計算最后結(jié)果的布局提供依據(jù)渺贤。
布局:DOM之后,需要將其中的元素對象同樣式信息結(jié)合起來请毛,計算它們的大小位置等布局信息志鞍,形成一個能夠表示這所有信息的內(nèi)部表示模型。
JavaScript引擎:JavaScript可以修改網(wǎng)頁的內(nèi)容方仿,也能修改CSS的信息固棚,JavaScript引擎解釋JavaScript代碼并把代碼的邏輯和對DOM和CSS的改動信息應(yīng)用到布局中去,從而改變渲染的結(jié)果仙蚜。
這些模塊依賴很多其他的基礎(chǔ)模塊此洲,這其中包括網(wǎng)絡(luò),存儲委粉,2D/3D圖形呜师,音頻視頻和圖片解碼器等。實際上贾节,渲染引擎中還應(yīng)該包括如何使用這些依賴模塊的部分汁汗,這部分的工作其實并不少,因為需要使用它們來高效的渲染網(wǎng)頁栗涂。例如知牌,利用2D/3D圖形庫來實現(xiàn)高性能的網(wǎng)頁繪制和網(wǎng)頁的3D渲染,這個實現(xiàn)非常非常的復(fù)雜斤程。最后角寸,當(dāng)然,在最下面,依然少不了操作系統(tǒng)的支持袭厂,例如線程支持墨吓,文件支持等等。
基本過程
瀏覽器是如何完成網(wǎng)頁渲染纹磺?
首先,我們回顧一下網(wǎng)頁渲染時亮曹,瀏覽器的動作:
根據(jù)來自服務(wù)器端的HTML代碼形成文檔對象模型(DOM)橄杨。
加載并解析樣式,形成CSS對象模型照卦。
在文檔對象模型和CSS對象模型之上式矫,創(chuàng)建一棵由一組待生成渲染的對象組成的渲染樹(在Webkit中這些對象被稱為渲染器或渲染對象,而在Gecko中稱之為“frame”役耕。)渲染樹反映了文檔對象模型的結(jié)構(gòu)采转,但是不包含諸如標(biāo)簽或含有display:none屬性的不可見元素。在渲染樹中瞬痘,每一段文本字符串都表現(xiàn)為獨立的渲染器故慈。每一個渲染對象都包含與之對應(yīng)的DOM對象,或者文本塊框全,還加上計算過的樣式察绷。換言之,渲染樹是一個文檔對象模型的直觀展示津辩。
對渲染樹上的每個元素拆撼,計算它的坐標(biāo),稱之為布局喘沿。瀏覽器采用一種流方法闸度,布局一個元素只需通過一次,但是表格元素需要通過多次蚜印。
最后莺禁,渲染樹上的元素最終展示在瀏覽器里,這一過程稱為“painting”晒哄。
當(dāng)用戶與網(wǎng)頁交互睁宰,或者腳本程序改動修改網(wǎng)頁時,前文提到的一些操作將會重復(fù)執(zhí)行寝凌,因為網(wǎng)頁的內(nèi)在結(jié)構(gòu)已經(jīng)發(fā)生了改變柒傻。
Repaint
當(dāng)改變那些不會影響元素在網(wǎng)頁中的位置的元素樣式時,譬如background-color(背景色)较木, border-color(邊框色)红符, visibility(可見性),瀏覽器只會用新的樣式將元素重繪一次(這就是重繪,或者說重新構(gòu)造樣式)预侯。
Reflow
當(dāng)改變影響到文本內(nèi)容或結(jié)構(gòu)致开,或者元素位置時,重排或者說重新布局就會發(fā)生萎馅。這些改變通常由以下事件觸發(fā):
DOM操作(元素添加双戳、刪除、修改或者元素順序的改變)糜芳;
內(nèi)容變化飒货,包括表單域內(nèi)的文本改變;
CSS屬性的計算或改變峭竣;
添加或刪除樣式表塘辅;
更改“類”的屬性;
瀏覽器窗口的操作(縮放皆撩,滾動)扣墩;
偽類激活(懸停)。
瀏覽器如何優(yōu)化渲染扛吞?
瀏覽器盡可能將 repaint/reflow 限制在被改變元素的區(qū)域內(nèi)呻惕。比如,對于位置固定或絕對的元素喻粹,其大小改變只影響元素本身及其子元素蟆融,然而,靜態(tài)定位元素的大小改變會觸發(fā)后續(xù)所有元素的重流守呜。
另一種優(yōu)化技巧是型酥,在運行幾段JavaScript代碼時,瀏覽器會緩存這些改變查乒,在代碼運行完畢后再將這些改變經(jīng)一次通過加以應(yīng)用弥喉。舉個例子,下面這段代碼只會觸發(fā)一個reflow和repaint:
var body=(‘body’);
body.css(‘padding′,‘1px′);//reflow,repaint
body.css(‘color’, ‘red’); // repaint
$body.css(‘margin’, ‘2px’);// reflow, repaint
// only 1 reflow and repaint will actually happen
然而玛迄,如前所述由境,改變元素的屬性會觸發(fā)強制性的重排。如果我們在上面的代碼塊中加入一行代碼蓖议,用來訪問元素的屬性虏杰,就會發(fā)生這種現(xiàn)象。
var body=(‘body’);
body.css(‘padding′,‘1px′);
body.css(‘padding’); // reading a property, a forced reflow
body.css(‘color′,‘red′);
body.css(‘margin’, ‘2px’);
其結(jié)果就是勒虾,重排發(fā)生了兩次纺阔。因此,你應(yīng)該把訪問元素屬性的操作都組織在一起修然,從而優(yōu)化網(wǎng)頁性能笛钝。
有時质况,你必須觸發(fā)一個強制性重排。比如玻靡,我們必須將同樣的屬性(比如左邊距)兩次賦值給同一個元素结榄。起初,它應(yīng)該設(shè)置為100px囤捻,且不帶動效臼朗。接著,它必須通過過渡(transition)動效改變?yōu)?0px最蕾。你現(xiàn)在可以在JSbin上學(xué)習(xí)這個例子依溯,不過我會在這兒更詳細地介紹它。
首先瘟则,我們創(chuàng)建一個帶過渡效果的CSS類:
.has-transition {
-webkit-transition: margin-left 1s ease-out;
-moz-transition: margin-left 1s ease-out;
-o-transition: margin-left 1s ease-out;
transition: margin-left 1s ease-out;
}
然后繼續(xù)執(zhí)行:
// our element that has a “has-transition” class by default
var targetElem=(‘#targetElemId’);
// remove the transition class
$targetElem.removeClass(‘has-transition’);
// change the property expecting the transition to be off, as the class is not there
// anymore
$targetElem.css(‘margin-left’, 100);
// put the transition class back
$targetElem.addClass(‘has-transition’);
// change the property
$targetElem.css(‘margin-left’, 50);
然而,這個執(zhí)行無法奏效枝秤。所有改變都被緩存醋拧,只在代碼塊末尾加以執(zhí)行。我們需要的是強制性的重排淀弹,我們可以通過以下更改加以實現(xiàn):
// remove the transition class
$(this).removeClass(‘has-transition’);
// change the property
$(this).css(‘margin-left’, 100);
// trigger a forced reflow, so that changes in a class/property get applied immediately
$(this)[0].offsetHeight; // an example, other properties would work, too
// put the transition class back
$(this).addClass(‘has-transition’);
// change the property
$(this).css(‘margin-left’, 50);
現(xiàn)在代碼如預(yù)期那樣執(zhí)行了丹壕。
二,性能優(yōu)化
創(chuàng)建有效的HTML和CSS文件薇溃,不要忘記指明文檔的編碼方式菌赖。樣式應(yīng)該包含在標(biāo)簽內(nèi),腳本代碼則應(yīng)該加在標(biāo)簽?zāi)┒恕?br> 盡量簡化和優(yōu)化CSS選擇器(這種優(yōu)化方式幾乎被使用CSS預(yù)處理器的開發(fā)者統(tǒng)一忽視了)將嵌套程度保持在最低水平沐序。以下是CSS選擇器的性能排名(從最快者開始):
- 識別器:#id
- 類:.class
- 標(biāo)簽:div
- 相鄰兄弟選擇器:a + i
- 父類選擇器:ul> li
- 通用選擇器:*
- 屬性選擇:input[type=”text”]
- 偽類和偽元素:a:hover
你應(yīng)該記住琉用,瀏覽器在處理選擇器時依照從右到左的原則,因此最右端的選擇器應(yīng)該是最快的:#id或則.class:
- div * {…} // bad
- .list li {…} // bad
- .list-item {…} // good
- list .list-item {…} // good
在你的腳本代碼中策幼,盡可能減少DOM操作邑时。緩存所有東西,包括元素屬性以及對象(如果它們被重用的話)特姐。當(dāng)進行復(fù)雜的操作時晶丘,使用“孤立”元素會更好,之后可以將其加到DOM中(所謂“孤立”元素是與DOM脫離唐含,僅保存在內(nèi)存中的元素)浅浮。
如果你使用jQuery來選擇元素,請遵從jQuery選擇器最佳實踐方案捷枯。
為了改變元素的樣式滚秩,修改“類”的屬性是奏效的方法之一。執(zhí)行這一改變時铜靶,處在DOM渲染樹的位置越深越好(這還有助于將邏輯與表象脫離)叔遂。
盡量只給位置絕對或者固定的元素添加動畫效果他炊。
在使用滾動時禁用復(fù)雜的懸停動效(比如,在中添加一個額外的不懸停類)
了解模塊之后已艰,下面就是這些模塊如何組織以達成渲染過程的痊末。一般地超全,一個典型的渲染過程下圖所示捷凄,這是渲染引擎的核心過程,一切都是圍繞著它來的昆淡。
guo
下面逐個從左至右來解釋上圖中的這一過程嚼吞。這一過程的先后關(guān)系由圖中的實線箭頭表示盒件。左上角開始,首先是網(wǎng)頁內(nèi)容舱禽,送到HTML解釋器炒刁。HTML解釋器在解釋它后形成DOM樹,中間如果遇到JavaScript代碼則交給JavaScript引擎去處理誊稚。如果頁面包含CSS翔始,則交給CSS解釋器去解析。當(dāng)DOM建立的時候里伯,接受來自CSS解釋的樣式信息城瞎,構(gòu)建一個新的內(nèi)部繪圖模型。該模型由布局模塊計算模型內(nèi)部的各個元素的位置和大小信息疾瓮,最后由繪圖模塊完成從該模型到圖像的繪制脖镀。
最后解釋圖中虛線箭頭的指向含義。它們表示在渲染過程中狼电,每個階段可能使用到的其他模塊蜒灰。在網(wǎng)頁內(nèi)容的下載中,需要使用到網(wǎng)絡(luò)和存儲漫萄,這個是顯而易見地卷员。但計算布局和繪圖的時候,需要使用2D/3D的圖形模塊腾务,同時因為要生成最后的可視化結(jié)果毕骡,這時候需要開始解碼音頻視頻和圖片,同其它內(nèi)容一起繪制到最后的圖像中岩瘦。
在渲染完成之后未巫,用戶可能需要跟渲染的結(jié)果進行交互,或者網(wǎng)頁自身有動畫启昧,一般而言叙凡,這會持續(xù)的重新渲染過程。這個過程跟上面類似密末,不再贅述握爷。
在此特別感謝inyiyi的博客跛璧,學(xué)習(xí)很多~inyiyi的博客