最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

瀏覽器渲染原理流程

2020-11-02 15:57 作者:廣州藍景  | 我要投稿

前言

瀏覽器的內(nèi)核是指支持瀏覽器運行的最核心的程序,分為兩個部分的,一是渲染引擎,另一個是JS引擎。渲染引擎在不同的瀏覽器中也不是都相同的。目前市面上常見的瀏覽器內(nèi)核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。這里面大家最耳熟能詳?shù)目赡芫褪?Webkit 內(nèi)核了,Webkit 內(nèi)核是當(dāng)下瀏覽器世界真正的霸主。

本文我們就以 Webkit 為例,對現(xiàn)代瀏覽器的渲染過程進行一個深度的剖析。


頁面加載過程

在介紹瀏覽器渲染過程之前,我們簡明扼要介紹下頁面的加載過程,有助于更好理解后續(xù)渲染過程。

要點如下:

  • 向瀏覽器輸入網(wǎng)址

  • 瀏覽器根據(jù) DNS 服務(wù)器得到域名的 IP 地址

  • 向這個 IP 的機器發(fā)送 HTTP 請求

  • 服務(wù)器收到、處理并返回 HTTP 請求

  • 瀏覽器接收到服務(wù)器返回的內(nèi)容

例如在瀏覽器輸入https://www.baidu.com,然后經(jīng)過?DNS 解析,www.baidu.com對應(yīng)的?IP 是14.215.177.38(不同時間、地點對應(yīng)的 IP 可能會不同)。然后瀏覽器向該 IP 發(fā)送 HTTP 請求。

服務(wù)端接收到 HTTP 請求,然后經(jīng)過計算(向不同的用戶推送不同的內(nèi)容),返回 HTTP 請求,返回的內(nèi)容如下:

其實就是一堆 HMTL 格式的字符串,因為只有 HTML 格式瀏覽器才能正確解析,這是 W3C 標(biāo)準(zhǔn)的要求。接下來就是瀏覽器的渲染過程。


瀏覽器渲染過程



從上面這個圖上,我們可以看到,瀏覽器渲染過程如下:

解析HTML,生成DOM樹,解析CSS,生成CSSOM樹

將DOM樹和CSSOM樹結(jié)合,生成渲染樹(Render Tree)

Layout(回流):根據(jù)生成的渲染樹,進行回流(Layout),得到節(jié)點的幾何信息(位置,大小)

Painting(重繪):根據(jù)渲染樹以及回流得到的幾何信息,得到節(jié)點的絕對像素

Display:?將像素發(fā)送給GPU,最后通過調(diào)用操作系統(tǒng)Native GUI的API繪制,展示在頁面上。(這一步其實還有很多內(nèi)容,比如會在GPU將多個合成層合并為同一個層,并展示在頁面中。而css3硬件加速的原理則是新建合成層,這里我們不展開,之后有機會再寫一篇博客來介紹)

渲染過程看起來也不復(fù)雜,讓我們來具體了解下每一步具體做了什么。


構(gòu)建DOM詳細流程

瀏覽器會遵守一套步驟將HTML 文件轉(zhuǎn)換為 DOM 樹。宏觀上,可以分為幾個步驟:



瀏覽器從磁盤或網(wǎng)絡(luò)讀取HTML的原始字節(jié)(字節(jié)數(shù)據(jù)),并根據(jù)文件的指定編碼(例如 UTF-8)將它們轉(zhuǎn)換成字符串。

在網(wǎng)絡(luò)中傳輸?shù)膬?nèi)容其實都是 0 和 1 這些字節(jié)數(shù)據(jù)。當(dāng)瀏覽器接收到這些字節(jié)數(shù)據(jù)以后,它會將這些字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符串,也就是我們寫的代碼。

將字符串轉(zhuǎn)換成Token,例如:、等。Token中會標(biāo)識出當(dāng)前Token是“開始標(biāo)簽”或是“結(jié)束標(biāo)簽”或著是“文本”等信息。

這時候你一定會有疑問,節(jié)點與節(jié)點之間的關(guān)系如何維護?

事實上,這就是Token要標(biāo)識“起始標(biāo)簽”和“結(jié)束標(biāo)簽”等標(biāo)識的作用。

例如“title”Token的起始標(biāo)簽和結(jié)束標(biāo)簽之間的節(jié)點肯定是屬于“head”的子節(jié)點。



上圖給出了節(jié)點之間的關(guān)系,例如:“Hello”Token位于“title”開始標(biāo)簽與“title”結(jié)束標(biāo)簽之間,表明“Hello”Token是“title”Token的子節(jié)點。同理“title”Token是“head”Token的子節(jié)點。

生成節(jié)點對象并構(gòu)建DOM

事實上,構(gòu)建DOM的過程中,不是等所有Token都轉(zhuǎn)換完成后再去生成節(jié)點對象,而是一邊生成Token一邊消耗Token來生成節(jié)點對象。換句話說,每個Token被生成后,會立刻消耗這個Token創(chuàng)建出節(jié)點對象。注意:帶有結(jié)束標(biāo)簽標(biāo)識的Token不會創(chuàng)建節(jié)點對象

接下來我們舉個例子,假設(shè)有段HTML文本:

<html>

<head> ?

<title>Web page parsing</title>

</head>

<body>

<div>

<h1>Web page parsing</h1> ? ?

<p>This is an example Web page.</p> ?

</div>

</body>

</html>

上面這段HTML會解析成這樣:


構(gòu)建CSSOM詳細流程

DOM會捕獲頁面的內(nèi)容,但瀏覽器還需要知道頁面如何展示,所以需要構(gòu)建CSSOM。

構(gòu)建CSSOM的過程與構(gòu)建DOM的過程非常相似,當(dāng)瀏覽器接收到一段CSS,瀏覽器首先要做的是識別出Token,然后構(gòu)建節(jié)點并生成CSSOM。

在這一過程中,瀏覽器會確定下每一個節(jié)點的樣式到底是什么,并且這一過程其實是很消耗資源的。因為樣式你可以自行設(shè)置給某個節(jié)點,也可以通過繼承獲得。在這一過程中,瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式。

注意:CSS匹配HTML元素是一個相當(dāng)復(fù)雜和有性能問題的事情。所以,DOM樹要小,CSS盡量用id和class,千萬不要過渡層疊下去。


構(gòu)建渲染樹

當(dāng)我們生成 DOM 樹和 CSSOM 樹以后,就需要將這兩棵樹組合為渲染樹。

在這一過程中,不是簡單的將兩者合并就行了。渲染樹只會包括需要顯示的節(jié)點和這些節(jié)點的樣式信息,如果某個節(jié)點是?display: none?的,那么就不會在渲染樹中顯示。

注意:渲染樹只包含可見的節(jié)點

我們或許有個疑惑:瀏覽器如果渲染過程中遇到JS文件怎么處理?

渲染過程中,如果遇到<script>就停止渲染,執(zhí)行 JS 代碼。因為瀏覽器有GUI渲染線程與JS引擎線程,為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,這兩個線程是互斥的關(guān)系。JavaScript的加載、解析與執(zhí)行會阻塞DOM的構(gòu)建,也就是說,在構(gòu)建DOM時,HTML解析器若遇到了JavaScript,那么它會暫停構(gòu)建DOM,將控制權(quán)移交給JavaScript引擎,等JavaScript引擎運行完畢,瀏覽器再從中斷的地方恢復(fù)DOM構(gòu)建。

也就是說,如果你想首屏渲染的越快,就越不應(yīng)該在首屏就加載 JS 文件,這也是都建議將 script 標(biāo)簽放在 body 標(biāo)簽底部的原因。當(dāng)然在當(dāng)下,并不是說 script 標(biāo)簽必須放在底部,因為你可以給 script 標(biāo)簽添加?defer(延遲)?或者?async(異步)?屬性(下文會介紹這兩者的區(qū)別)。

JS文件不只是阻塞DOM的構(gòu)建,它會導(dǎo)致CSSOM也阻塞DOM的構(gòu)建。

原本DOM和CSSOM的構(gòu)建是互不影響,井水不犯河水,但是一旦引入了JavaScript,CSSOM也開始阻塞DOM的構(gòu)建,只有CSSOM構(gòu)建完畢后,DOM再恢復(fù)DOM構(gòu)建。

這是什么情況呢?

這是因為JavaScript不只是可以改DOM,它還可以更改樣式,也就是它可以更改CSSOM。因為不完整的CSSOM是無法使用的,如果JavaScript想訪問CSSOM并更改它,那么在執(zhí)行JavaScript時,必須要能拿到完整的CSSOM。所以就導(dǎo)致了一個現(xiàn)象,如果瀏覽器尚未完成CSSOM的下載和構(gòu)建,而我們卻想在此時運行腳本,那么瀏覽器將延遲腳本執(zhí)行和DOM構(gòu)建,直至其完成CSSOM的下載和構(gòu)建。也就是說,在這種情況下,瀏覽器會先下載和構(gòu)建CSSOM,然后再執(zhí)行JavaScript,最后在繼續(xù)構(gòu)建DOM。

布局與繪制

當(dāng)瀏覽器生成渲染樹以后,就會根據(jù)渲染樹來進行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個節(jié)點在頁面中的確切位置和大小。通常這一行為也被稱為“自動重排”。

布局流程的輸出是一個“盒模型”,它會精確地捕獲每個元素在視口內(nèi)的確切位置和尺寸,所有相對測量值都將轉(zhuǎn)換為屏幕上的絕對像素。

布局完成后,瀏覽器會立即發(fā)出“Paint Setup”和“Paint”事件,將渲染樹轉(zhuǎn)換成屏幕上的像素。


回流

前面我們通過構(gòu)造渲染樹,我們將可見DOM節(jié)點以及它對應(yīng)的樣式結(jié)合起來,可是我們還需要計算它們在設(shè)備視口(viewport)內(nèi)的確切位置和大小,這個計算的階段就是回流。

為了弄清每個對象在網(wǎng)站上的確切大小和位置,瀏覽器從渲染樹的根節(jié)點開始遍歷,我們可以以下面這個實例來表示:

<!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>

我們可以看到,第一個div將節(jié)點的顯示尺寸設(shè)置為視口寬度的50%,第二個div將其尺寸設(shè)置為父節(jié)點的50%。而在回流這個階段,我們就需要根據(jù)視口具體的寬度,將其轉(zhuǎn)為實際的像素值。(如下圖)

我們可以看到,第一個div將節(jié)點的顯示尺寸設(shè)置為視口寬度的50%,第二個div將其尺寸設(shè)置為父節(jié)點的50%。而在回流這個階段,我們就需要根據(jù)視口具體的寬度,將其轉(zhuǎn)為實際的像素值。(如下圖)


重繪

最終,我們通過構(gòu)造渲染樹和回流階段,我們知道了哪些節(jié)點是可見的,以及可見節(jié)點的樣式和具體的幾何信息(位置、大小),那么我們就可以將渲染樹的每個節(jié)點都轉(zhuǎn)換為屏幕上的實際像素,這個階段就叫做重繪節(jié)點。

既然知道了瀏覽器的渲染過程后,我們就來探討下,何時會發(fā)生回流重繪。


何時發(fā)生回流重繪

我們前面知道了,回流這一階段主要是計算節(jié)點的位置和幾何信息,那么當(dāng)頁面布局和幾何信息發(fā)生變化的時候,就需要回流。

比如以下情況發(fā)生回流:

根據(jù)改變的范圍和程度,渲染樹中或大或小的部分需要重新計算,有些改變會觸發(fā)整個頁面的重排,比如,滾動條出現(xiàn)的時候或者修改了根節(jié)點。

  • 頁面一開始渲染的時候(這肯定避免不了)

  • 瀏覽器的窗口尺寸變化(因為回流是根據(jù)視口的大小來計算元素的位置和大小的)

  • 添加或刪除可見的DOM元素

  • 元素的位置發(fā)生變化

  • 元素的尺寸發(fā)生變化(包括外邊距、內(nèi)邊框、邊框大小、高度和寬度等)

  • 內(nèi)容發(fā)生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代。

  • 元素字體大小變化

  • 激活CSS偽類(例如::hover)

一些常用且會導(dǎo)致回流的屬性和方法:

clientWidth、clientHeight、clientTop、clientLeft?

offsetWidth、offsetHeight、offsetTop、offsetLeft?

scrollWidth、scrollHeight、scrollTop、scrollLeft?

scrollIntoView()、scrollIntoViewIfNeeded()?

getComputedStyle()

getBoundingClientRect()?

scrollTo()Copy to clipboardErrorCopied

以下情況發(fā)生重繪而不回流

當(dāng)頁面中元素樣式的改變并不影響它在文檔流中的位置時(例如:color、background-color、visibility等),瀏覽器會將新樣式賦予給元素并重新繪制它,這個過程重繪而不回流。

注意:回流一定會觸發(fā)重繪,而重繪不一定會回流


性能影響

回流比重繪的代價要更高。

有時即使僅僅回流一個單一的元素,它的父元素以及任何跟隨它的元素也會產(chǎn)生回流。


瀏覽器優(yōu)化機制

現(xiàn)代瀏覽器會對頻繁的回流或重繪操作進行優(yōu)化:

瀏覽器會維護一個隊列,把所有引起回流和重繪的操作放入隊列中,如果隊列中的任務(wù)數(shù)量或者時間間隔達到一個閾值的,瀏覽器就會將隊列清空,進行一次批處理,這樣可以把多次回流和重繪變成一次。

當(dāng)你訪問以下屬性或方法時,瀏覽器會立刻清空隊列:

clientWidth、clientHeight、clientTop、clientLeft?

offsetWidth、offsetHeight、offsetTop、offsetLeft

scrollWidth、scrollHeight、scrollTop、scrollLeft?

width、height?

getComputedStyle()

getBoundingClientRect()


因為隊列中可能會有影響到這些屬性或方法返回值的操作,即使你希望獲取的信息與隊列中操作引發(fā)的改變無關(guān),瀏覽器也會強行清空隊列,確保你拿到的值是最精確的。

以上屬性和方法都需要返回最新的布局信息,因此瀏覽器不得不清空隊列,觸發(fā)回流重繪來返回正確的值。因此,我們在修改樣式的時候,最好避免使用上面列出的屬性,他們都會刷新渲染隊列。如果要使用它們,最好將值緩存起來。


減少回流和重繪

  • 使用 transform 替代 top

  • 使用 visibility 替換 display: none ,因為前者只會引起重繪,后者會引發(fā)回流(改變了布局)

  • 不要把節(jié)點的屬性值放在一個循環(huán)里當(dāng)成循環(huán)里的變量。

  • 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局

  • 動畫實現(xiàn)的速度的選擇,動畫速度越快,回流次數(shù)越多,也可以選擇使用 requestAnimationFrame

  • CSS 選擇符從右往左匹配查找,避免節(jié)點層級過多

  • 將頻繁重繪或者回流的節(jié)點設(shè)置為圖層,圖層能夠阻止該節(jié)點的渲染行為影響別的節(jié)點。比如對于 video 標(biāo)簽來說,瀏覽器會自動將該節(jié)點變?yōu)閳D層。

最小化回流和重繪

由于回流和重繪可能代價比較昂貴,因此最好就是可以減少它的發(fā)生次數(shù)。為了減少發(fā)生次數(shù),我們可以合并多次對DOM和樣式的修改,然后一次處理掉??紤]這個例子

const el = document.getElementById('test');

el.style.padding = '5px';

el.style.borderLeft = '1px';

el.style.borderRight = '2px';


例子中,有三個樣式屬性被修改了,每一個都會影響元素的幾何結(jié)構(gòu),引起回流。當(dāng)然,大部分現(xiàn)代瀏覽器都對其做了優(yōu)化,因此,只會觸發(fā)一次重排。但是如果在舊版的瀏覽器或者在上面代碼執(zhí)行的時候,有其他代碼訪問了布局信息(上文中的會觸發(fā)回流的布局信息),那么就會導(dǎo)致三次重排。

因此,我們可以合并所有的改變?nèi)缓笠来翁幚?,比如我們可以采取以下的方式?/p>

  1. 使用cssText

const el = document.getElementById('test');?

el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';


? ? 2.使用class, 把css樣式用個class包住,修改CSS的class.

active{ ?

border-left: 1px; ?

border-right: 2px; ?

?padding: 5px;?

}

const el = document.getElementById('test');

el.className += ' active';


批量修改DOM

當(dāng)我們需要對DOM對一系列修改的時候,可以通過以下步驟減少回流重繪次數(shù):

  • 使元素脫離文檔流

  • 對其進行多次修改

  • 將元素帶回到文檔中。

該過程的第一步和第三步可能會引起回流,但是經(jīng)過第一步之后,對DOM的所有修改都不會引起回流,因為它已經(jīng)不在渲染樹了。

有三種方式可以讓DOM脫離文檔流:

  • 隱藏元素,應(yīng)用修改,重新顯示

  • 使用文檔片段(document fragment)在當(dāng)前DOM之外構(gòu)建一個子樹,再把它拷貝回文檔。

  • 將原始元素拷貝到一個脫離文檔的節(jié)點中,修改節(jié)點后,再替換原始的元素。

下面來個例子演示下


我們要執(zhí)行一段批量插入節(jié)點的代碼:

function appendDataToElement(appendToElement, data) {?

? ? let li; ?

? for (let i = 0; i < data.length; i++) { ?

? ? li = document.createElement('li'); ? ??

? ? li.textContent = 'text'; ??

? ? appendToElement.appendChild(li); ?

? ? ?}?

}?

const ul = document.getElementById('list');

appendDataToElement(ul, data);


如果我們直接這樣執(zhí)行的話,由于每次循環(huán)都會插入一個新的節(jié)點,會導(dǎo)致瀏覽器回流一次。

我們可以使用這三種方式進行優(yōu)化:


隱藏元素,應(yīng)用修改,重新顯示

第一種方法:隱藏元素,這個會在展示和隱藏節(jié)點的時候,產(chǎn)生兩次重繪

function appendDataToElement(appendToElement, data) { ?

?let li; ??

?for (let i = 0; i < data.length; i++) { ??

? ? ?li = document.createElement('li'); ? ?

?? ? li.textContent = 'text'; ? ?

? ? appendToElement.appendChild(li); ?

? ?}

}

const ul = document.getElementById('list');

ul.style.display = 'none';

appendDataToElement(ul, data);

ul.style.display = 'block';


第二種:使用文檔片段(document fragment)在當(dāng)前DOM之外構(gòu)建一個子樹,再把它拷貝回文檔

const ul = document.getElementById('list');

const fragment = document.createDocumentFragment();?

appendDataToElement(fragment, data);

ul.appendChild(fragment);


第三種:將原始元素拷貝到一個脫離文檔的節(jié)點中,修改節(jié)點后,再替換原始的元素。

const ul = document.getElementById('list');

const clone = ul.cloneNode(true);

appendDataToElement(clone, data);

ul.parentNode.replaceChild(clone, ul);


避免觸發(fā)同步布局事件

上文我們說過,當(dāng)我們訪問元素的一些屬性的時候,會導(dǎo)致瀏覽器強制清空隊列,進行強制同步布局。舉個例子,比如說我們想將一個p標(biāo)簽數(shù)組的寬度賦值為一個元素的寬度,我們可能寫出這樣的代碼:

function initP() { ??

? for (let i = 0; i < paragraphs.length; i++) { ? ?

? ? paragraphs[i].style.width = box.offsetWidth + 'px'; ??

??? }?

}


這段代碼看上去是沒有什么問題,可是其實會造成很大的性能問題。在每次循環(huán)的時候,都讀取了box的一個offsetWidth屬性值,然后利用它來更新p標(biāo)簽的width屬性。這就導(dǎo)致了每一次循環(huán)的時候,瀏覽器都必須先使上一次循環(huán)中的樣式更新操作生效,才能響應(yīng)本次循環(huán)的樣式讀取操作。每一次循環(huán)都會強制瀏覽器刷新隊列。我們可以優(yōu)化為:

const width = box.offsetWidth;?

function initP() {

? ? for (let i = 0; i < paragraphs.length; i++) { ? ? ? ? paragraphs[i].style.width = width + 'px'; ?

? }?

}

對于復(fù)雜動畫效果,使用絕對定位讓其脫離文檔流

對于復(fù)雜動畫效果,由于會經(jīng)常的引起回流重繪,因此,我們可以使用絕對定位,讓它脫離文檔流。否則會引起父元素以及后續(xù)元素頻繁的回流。這個我們就直接上個例子。

打開這個例子后,我們可以打開控制臺,控制臺上會輸出當(dāng)前的幀數(shù)(雖然不準(zhǔn))。

從例子中,我們可以看到,幀數(shù)一直都沒到60。這個時候,只要我們點擊一下那個按鈕,把這個元素設(shè)置為絕對定位,幀數(shù)就可以穩(wěn)定60。

css3硬件加速(GPU加速)

比起考慮如何減少回流重繪,我們更期望的是,根本不要回流重繪。這個時候,css3硬件加速就閃亮登場啦?。?/p>

劃重點:使用css3硬件加速,可以讓transform、opacity、filters這些動畫不會引起回流重繪 。但是對于動畫的其它屬性,比如background-color這些,還是會引起回流重繪的,不過它還是可以提升這些動畫的性能。

如何使用css3硬件加速(GPU加速)

常見的觸發(fā)硬件加速的css3屬性:

  • transform

  • opacity

  • filters

  • Will-change

css3硬件加速的坑

  • 如果你為太多元素使用css3硬件加速,會導(dǎo)致內(nèi)存占用較大,會有性能問題。

  • 在GPU渲染字體會導(dǎo)致抗鋸齒無效。這是因為GPU和CPU的算法不同。因此如果你不在動畫結(jié)束的時候關(guān)閉硬件加速,會產(chǎn)生字體模糊。














瀏覽器渲染原理流程的評論 (共 條)

分享到微博請遵守國家法律
高密市| 永康市| 赫章县| 徐州市| 化州市| 阳西县| 商丘市| 五台县| 连江县| 常德市| 郯城县| 休宁县| 石台县| 襄樊市| 万载县| 公安县| 靖宇县| 讷河市| 同江市| 郯城县| 黄平县| 焦作市| 深泽县| 邛崃市| 定远县| 缙云县| 锡林郭勒盟| 阿尔山市| 潼关县| 上饶县| 黄山市| 四子王旗| 溧阳市| 定襄县| 新巴尔虎左旗| 常山县| 定南县| 东乡| 隆回县| 莒南县| 商城县|