本文主要包含以下內(nèi)容:
瀏覽器渲染整體流程
解析 HTML
樣式計算
布局
分層
生成繪制指令
分塊
光柵化
繪制
常見面試題
瀏覽器渲染整體流程
瀏覽器,作為用戶瀏覽網(wǎng)頁最基本的一個入口,我們似乎認(rèn)為在地址欄輸入 URL 后網(wǎng)頁自動就出來了。殊不知在用戶輸入網(wǎng)頁地址,敲下回車的那一刻,瀏覽器背后做了諸多的事情。
去除 DNS 查找等這些細(xì)枝末節(jié)的工作,整個大的部分可以分為兩個,那就是 網(wǎng)絡(luò)和 渲染。
總體流程粗略概覽
1、瀏覽器查找域名對應(yīng)的 IP 地址(DNS 查詢:瀏覽器緩存->系統(tǒng)緩存->路由器緩存->ISP DNS 緩存->根域名服務(wù)器) 2、瀏覽器向 Web 服務(wù)器發(fā)送一個 HTTP 請求(TCP 三次握手) 3、服務(wù)器 301 重定向(從 HTTP://example.com 重定向到 HTTP://www.example.com)
4、瀏覽器跟蹤重定向地址,請求另一個帶 www 的網(wǎng)址 5、服務(wù)器處理請求(通過路由讀取資源) 6、服務(wù)器返回一個 HTTP 響應(yīng)(報頭中把 Content-type 設(shè)置為 'text/html')
7、瀏覽器進(jìn) DOM 樹構(gòu)建 8、瀏覽器發(fā)送請求獲取嵌在 HTML 中的資源(如圖片、音頻、視頻、CSS、JS 等)9、瀏覽器顯示完成頁面 10、瀏覽器發(fā)送異步請求

首先,瀏覽器的網(wǎng)絡(luò)線程會發(fā)送 http 請求,和服務(wù)器之間進(jìn)行通信,之后將拿到的 html 封裝成一個渲染任務(wù),并將其傳遞給渲染主線程的消息隊列。在事件循環(huán)機(jī)制的作用下,渲染主線程取出消息隊列中的渲染任務(wù),開啟渲染流程。
網(wǎng)絡(luò)線程和服務(wù)器之間通信的過程主要是三次握手建立tcp鏈接,服務(wù)器收到請求后返回響應(yīng)報文,但是本文主要講述瀏覽器的渲染進(jìn)程如何將一個密密麻麻的 html 字符串渲染成最終頁面的。
我們先來看一下整體流程,整個渲染流程分為多個階段,分別是: HTML 解析、樣式計算、布局、分層、生成繪制指令、分塊、光柵化、繪制:

每個階段都有明確的輸入輸出,上一個階段的輸出會成為下一個階段的輸入。
這樣,整個渲染流程就形成了一套組織嚴(yán)密的生產(chǎn)流水線。
接下來,咱們就一起來看一下每一個階段的各個流程究竟是在干什么。
解析 HTML
首先第一步就是解析 html,生成 DOM 樹。
當(dāng)我們打開一個網(wǎng)頁時,瀏覽器都會去請求對應(yīng)的 HTML 文件。雖然平時我們寫代碼時都會分為 HTML、CSS、JS 文件,也就是字符串,但是計算機(jī)硬件是不理解這些字符串的,所以在網(wǎng)絡(luò)中傳輸?shù)膬?nèi)容其實都是 0 和 1 這些字節(jié)數(shù)據(jù)。
當(dāng)瀏覽器接收到這些字節(jié)數(shù)據(jù)以后,它會將這些字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符串,也就是我們寫的代碼。

當(dāng)數(shù)據(jù)轉(zhuǎn)換為字符串以后,瀏覽器會先將這些字符串通過詞法分析轉(zhuǎn)換為標(biāo)記( token ),這一過程在詞法分析中叫做標(biāo)記化( tokenization )。
為什么需要標(biāo)記化呢?原因很簡單,現(xiàn)在瀏覽器雖然將字節(jié)數(shù)據(jù)轉(zhuǎn)為了字符串,但是此時的字符串就如何一篇標(biāo)題段落全部寫在一行的文章一樣,瀏覽器此時仍然是不能理解的。
例如:
<!DOCTYPE html><htmllang="en"><head><title>Document</title></head><body><p>this is a test</p></body></html>
因此現(xiàn)在所做的標(biāo)記化,本質(zhì)就是要將這長長的字符串分拆成一塊塊,并給這些內(nèi)容打上標(biāo)記,便于理解這些最小單位的代碼是什么意思。

將整個字符串進(jìn)行了標(biāo)記化之后,就能夠在此基礎(chǔ)上構(gòu)建出對應(yīng)的 DOM 樹出來。

上面的步驟,我們就稱之為解析 HTML。整個流程如下圖:

在解析 HTML 的過程中,我們可以能會遇到諸如 style、link 這些標(biāo)簽,聰明的你應(yīng)該已經(jīng)想到了,這是和我們網(wǎng)頁樣式相關(guān)的內(nèi)容。此時就會涉及到 CSS 的解析。
為了提高解析效率,瀏覽器在開始解析前,會啟動一個預(yù)解析的線程,率先下載 HTML 中的外部 CSS 文件和外部的 JS 文件。
如果主線程解析到 link 位置,此時外部的 CSS 文件還沒有下載解析好,主線程不會等待,繼續(xù)解析后續(xù)的 HTML。這是因為下載和解析 CSS 的工作是在預(yù)解析線程中進(jìn)行的。這就是 CSS 不會阻塞 HTML 解析的根本原因。

最終,CSS 的解析在經(jīng)歷了從字節(jié)數(shù)據(jù)、字符串、標(biāo)記化后,最終也會形成一顆 CSSOM 樹。

上面也有提到,預(yù)解析線程除了下載外部 CSS 文件以外,還會下載外部 JS 文件,那么這里同學(xué)們自然也會好奇針對 JS 代碼瀏覽器是如何處理的?
如果主線程解析到 script 位置,會停止解析 HTML,轉(zhuǎn)而等待 JS 文件下載好,并將全局代碼解析執(zhí)行完成后,才能繼續(xù)解析 HTML。
為什么呢?
這是因為 JS 代碼的執(zhí)行過程可能會修改當(dāng)前的 DOM 樹,所以 DOM 樹的生成必須暫停。這就是 JS 會阻塞 HTML 解析的根本原因。

因此,如果你想首屏渲染的越快,就越不應(yīng)該在最前面就加載 JS 文件,這也是都建議將 script 標(biāo)簽放在 body 標(biāo)簽底部的原因。
<html>
<head>
...
</head>
<body>
<p></p>
<scriptsrc="..."></script>
</body>
</html>
另外,在現(xiàn)代瀏覽器中,為我們提供了新的方式來避免 JS 代碼阻塞渲染的情況:
async
defer
prefetch
preload
關(guān)于這幾種方式的區(qū)別,我們在另外一篇文章中再具體來看。
最后總結(jié)一下此階段的成果,第一步完成后,會得到 DOM 樹和 CSSOM 樹,瀏覽器的默認(rèn)樣式、內(nèi)部樣式、外部樣式、行內(nèi)樣式均會包含在 CSSOM 樹中。
得到了兩棵樹,如下圖所示:

樣式計算
接下來進(jìn)入第二步:樣式計算
擁有了 DOM 樹我們還不足以知道頁面的外貌,因為我們通常會為頁面的元素設(shè)置一些樣式。主線程會遍歷得到的 DOM 樹,依次為樹中的每個節(jié)點計算出它最終的樣式,稱之為 Computed Style。
在這一過程中,很多預(yù)設(shè)值會變成絕對值,比如 red 會變成 rgb(255,0,0);相對單位會變成絕對單位,比如 em 會變成 px。

瀏覽器會確定每一個節(jié)點的樣式到底是什么,并最終生成一顆樣式規(guī)則樹,這棵樹上面記錄了每一個 DOM 節(jié)點的樣式。
另外需要注意的是,這里所指的瀏覽器確定每一個節(jié)點的樣式,是指在樣式計算時會對所有的 DOM 節(jié)點計算出所有的樣式屬性值。如果開發(fā)者在書寫樣式時,沒有寫某一項樣式,那么大概率會使用其默認(rèn)值。例如:

關(guān)于樣式計算的詳細(xì)過程,請參閱文章《CSS 屬性計算過程》。
這一步完成后,我們就得到一棵帶有樣式的 DOM 樹。也就是說,經(jīng)過樣式計算后,之前的 DOM 數(shù)和 CSSOM 數(shù)合并成了一顆帶有樣式的 DOM 樹。

布局
前面這些步驟完成之后,渲染進(jìn)程就已經(jīng)知道頁面的具體文檔結(jié)構(gòu)以及每個節(jié)點擁有的樣式信息了,可是這些信息還是不能最終確定頁面的樣子。
舉個例子,假如你現(xiàn)在想通過電話告訴你的朋友你身邊的一幅畫的內(nèi)容:“畫布上有一個紅色的大圓圈和一個藍(lán)色的正方形”,單憑這些信息你的朋友是很難知道這幅畫具體是什么樣子的,因為他不知道大圓圈和正方形具體在頁面的什么位置,是正方形在圓圈前面呢還是圓圈在正方形的前面。

渲染網(wǎng)頁也是同樣的道理,只知道網(wǎng)站的文檔流以及每個節(jié)點的樣式是遠(yuǎn)遠(yuǎn)不足以渲染出頁面內(nèi)容的,還需要通過布局(layout)來計算出每個節(jié)點的幾何信息(geometry)。
生成布局樹的具體過程是:主線程會遍歷剛剛構(gòu)建的 DOM 樹,根據(jù) DOM 節(jié)點的計算樣式計算出一個布局樹(layout tree)。布局樹上每個節(jié)點會有它在頁面上的 x,y 坐標(biāo)以及盒子大小(bounding box sizes)的具體信息。

布局樹大部分時候,和 DOM 樹并非一一對應(yīng)。雖然它長得和先前構(gòu)建的 DOM 樹差不多,但是不同的是這顆樹只有那些可見的(visible)節(jié)點信息。
比如 display:none 的節(jié)點沒有幾何信息,因此不會生成到布局樹;

又比如使用了偽元素選擇器,雖然 DOM 樹中不存在這些偽元素節(jié)點,但它們擁有幾何信息,所以會生成到布局樹中。

還有匿名行盒、匿名塊盒等等都會導(dǎo)致 DOM 樹和布局樹無法一一對應(yīng)。

分層
在確認(rèn)了布局樹后,接下來就是繪制了么?
還不急,這里還會有一個步驟,就是分層。

分層的好處在于,將來某一個層改變后,僅會對該層進(jìn)行后續(xù)處理,從而提升效率。
為了確定哪些元素需要放置在哪一層,主線程需要遍歷整顆布局樹來創(chuàng)建一棵層次樹(Layer Tree)

滾動條、堆疊上下文、transform、opacity 等樣式都會或多或少的影響分層結(jié)果,也可以通過使用 will-change 屬性來告訴瀏覽器對其分層。
生成繪制指令
分層工作結(jié)束后,接下來就是生成繪制指令。
主線程會為每個層單獨產(chǎn)生繪制指令集,用于描述這一層的內(nèi)容該如何畫出來。

這里的繪制指令,類似于“將畫筆移動到 xx 位置,放下畫筆,繪制一條 xx 像素長度的線”,我們在瀏覽器所看到的各種復(fù)雜的頁面,實際上都是這樣一條指令一條指令的執(zhí)行所繪制出來的。
如果你熟悉 Canvas,那么這樣的指令類似于:
context.beginPath(); // 開始路徑
context.moveTo(10, 10); // 移動畫筆
context.lineTo(100, 100); // 繪畫出一條直線
context.closePath(); // 閉合路徑
context.stroke(); // 進(jìn)行勾勒
但是你要注意,這一步只是生成諸如上面代碼的這種繪制指令集,還沒有開始執(zhí)行這些指令。
另外,還有一個重要的點你需要知道,生成繪制指令集后,渲染主線程的工程就暫時告一段落,接下來主線程將每個圖層的繪制信息提交給合成線程,剩余工作將由合成線程完成。

分塊
合成線程首先對每個圖層進(jìn)行分塊,將其劃分為更多的小區(qū)域。

此時,它不再是像主線程那樣一個人在戰(zhàn)斗,它會從線程池中拿取多個線程來完成分塊工作。

光柵化
分塊完成后,進(jìn)入光柵化階段。所謂光柵化,就是將每個塊變成位圖。
更簡單的理解就是確認(rèn)每一個像素點的 rgb 信息,如下圖所示:

光柵化的操作,并不由合成線程來做,而是會由合成線程將塊信息交給 GPU 進(jìn)程,以極高的速度完成光柵化。

GPU 進(jìn)程會開啟多個線程來完成光柵化,并且優(yōu)先處理靠近視口區(qū)域的塊。

繪制
最后一步,我們總算迎來了真正的繪制。
當(dāng)所有的圖塊都被柵格化后,合成線程會拿到每個層、每個塊的位圖,從而生成一個個「指引(quad)」信息。

指引會標(biāo)識出每個位圖應(yīng)該畫到屏幕的哪個位置,以及會考慮到旋轉(zhuǎn)、縮放等變形。
變形發(fā)生在合成線程,與渲染主線程無關(guān),這就是 transform 效率高的本質(zhì)原因。
合成線程會通過 IPC 向瀏覽器進(jìn)程(browser process)提交(commit)一個渲染幀。這個時候可能有另外一個合成幀被瀏覽器進(jìn)程的 UI線程(UI thread)提交以改變?yōu)g覽器的 UI。這些合成幀都會被發(fā)送給 GPU 完成最終的屏幕成像。
如果合成線程收到頁面滾動的事件,合成線程會構(gòu)建另外一個合成幀發(fā)送給 GPU 來更新頁面。

最后總結(jié)一下瀏覽器從拿到 html 文檔到最終渲染出頁面的整體流程,如下圖:

常見面試題
什么是 reflow?
reflow 的本質(zhì)就是重新計算 layout 樹。
當(dāng)進(jìn)行了會影響布局樹的操作后,需要重新計算布局樹,會引發(fā) layout。
為了避免連續(xù)的多次操作導(dǎo)致布局樹反復(fù)計算,瀏覽器會合并這些操作,當(dāng) JS 代碼全部完成后再進(jìn)行統(tǒng)一計算。所以,改動屬性造成的 reflow 是異步完成的。
也同樣因為如此,當(dāng) JS 獲取布局屬性時,就可能造成無法獲取到最新的布局信息。
瀏覽器在反復(fù)權(quán)衡下,最終決定獲取屬性立即 reflow。
什么是 repaint?文章來源:http://www.zghlxwxcb.cn/news/detail-758618.html
repaint 的本質(zhì)就是重新根據(jù)分層信息計算了繪制指令。
當(dāng)改動了可見樣式后,就需要重新計算,會引發(fā) repaint。
由于元素的布局信息也屬于可見樣式,所以 reflow 一定會引起 repaint。
為什么 transform 的效率高?文章來源地址http://www.zghlxwxcb.cn/news/detail-758618.html
因為 transform 既不會影響布局也不會影響繪制指令,它影響的只是渲染流程的最后一個「draw」階段
由于 draw 階段在合成線程中,所以 transform 的變化幾乎不會影響渲染主線程。反之,渲染主線程無論如何忙碌,也不會影響 transform 的變化。
到了這里,關(guān)于【前端】瀏覽器的渲染流程(完整)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!