瀏覽器渲染原理
前言
當(dāng)我們輸入一個url之后,頁面會在很短時(shí)間內(nèi)呈現(xiàn)給我們。這個過程其實(shí)相當(dāng)復(fù)雜,瀏覽器在背后給我做了很多的事。除去一些諸如DNS解析的等邊角料工作,整體可以大致分為網(wǎng)絡(luò)和渲染。
渲染
瀏覽器的網(wǎng)絡(luò)線程接收到HTML文檔后,會生成一個渲染任務(wù),并將其添加到渲染主線程的消息隊(duì)列。在事件循環(huán)機(jī)制的作用下,渲染主線程會取出消息隊(duì)列中的渲染任務(wù),開啟渲染流程。

整個渲染流程分為多個階段:解析HTML、計(jì)算樣式、布局、分層、繪制、分塊、光柵化、畫。每個階段都有明確的輸入輸出,上一階段的輸出會成為下一階段的輸入,這樣整個渲染過程就形成了一條組織嚴(yán)密的生產(chǎn)流水線。

解析HTML-Parse HTML
第一步就是解析 HTML,生成 DOM 樹。
在主線程上解析HTML字符串,得到DOM樹(html元素、文本、注釋等節(jié)點(diǎn)的信息)和CSSOM樹。

CSSOM樹會包含瀏覽器默認(rèn)樣式、內(nèi)部樣式、外部樣式、內(nèi)聯(lián)樣式:

在解析過程中,遇到CSS就解析CSS,遇到JS就執(zhí)行JS。為了提高解析效率,瀏覽器會在解析之前,啟動一個預(yù)解析線程,率先下載外部的CSS文件和JS文件。
如果主線程解析到link的位置,此時(shí)link的CSS資源文件還沒下載解析好,主線程不會等待,繼續(xù)解析后面的HTML。這是因?yàn)橄螺d和解析CSS是在預(yù)解析線程中進(jìn)行的,這就是CSS不會阻塞HTML解析的原因。

因此,如果我們想加快首屏的渲染,建議將 script 標(biāo)簽放在 body 標(biāo)簽底部。
當(dāng)然現(xiàn)代瀏覽器都提供了非阻塞的下載方式,async和defer。
得到每個節(jié)點(diǎn)計(jì)算后的最終樣式,如下圖,我們可以看到任何元素都會有全量的CSS屬性:
? 確定聲明值
? 層疊沖突 (重要性、特殊性、源次性)
? 使用繼承
? 使用默認(rèn)值
渲染主線程會遍歷整棵DOM樹,依次計(jì)算出DOM樹的每個節(jié)點(diǎn)的最終樣式,稱為 Computed Style。在這個過程,很多預(yù)設(shè)值會變成絕對值,相對單位會變成絕對單位。這一步完成之后,將會得到一棵帶有樣式的DOM樹。

根據(jù)每個節(jié)點(diǎn)的樣式信息算出節(jié)點(diǎn)的幾何信息(尺寸和位置),得到布局(Layout)樹。
對于一個元素來說,它的尺寸和位置經(jīng)常與它的包含塊(containing block)有關(guān),即我們經(jīng)常說的它是相對于哪個元素,例如 width: 100% 。
如何確定包含塊?
確定一個元素的包含塊的過程完全依賴于這個元素的position屬性:
? static、relative、sticky:包含塊可能由它的最近的祖先塊元素(如 inline-block、block )
? absolute:由它的最近的 position 的值不是 static 的祖先元素
? fixed:在連續(xù)媒體的情況下包含塊是viewport
? absolute或fixed:包含塊也可能是由滿足以下條件的最近父級元素
○ transform或 perspective 的值不是 none。
○ will-change的值是 transform 或 perspective。
○ filter的值不是 none
○ contain的值是 paint
○ backdrop-filter的值不是 none

DOM樹和Layout樹不一定是一一對應(yīng)的,如隱藏(dispay: none)的元素就不會出現(xiàn)在Layout樹中;
又如偽元素在DOM樹中并不存在,但是會出現(xiàn)在Layout樹中,因?yàn)樗鼡碛袔缀涡畔ⅰ?/span>

文本內(nèi)容必須在行盒中,行盒和塊盒不能相鄰。
如果在塊盒中直接寫入內(nèi)容,則會在中間生成一個匿名行盒;如果塊盒和行盒相鄰,則為行盒外部生成一個匿名塊盒。
重排(Reflow) 的本質(zhì)就是重新計(jì)算Layout布局樹。當(dāng)進(jìn)行了會影響布局樹的操作后,需要重新計(jì)算布局樹,就會引發(fā)重新Layout。 瀏覽器為了避免連續(xù)的多次操作導(dǎo)致布局樹反復(fù)計(jì)算,就會合并這些操作,生成一個渲染任務(wù),等到下一次事件循環(huán)再進(jìn)行計(jì)算。所以,改動CSS屬性所造成的Reflow是異步完成的。 正因?yàn)槿绱?,?dāng) JS 獲取布局屬性時(shí)(如clientWidth),就可能造成無法獲取到最新的布局信息。 于是瀏覽器在反復(fù)權(quán)衡下,最終決定獲取屬性時(shí),立即 Reflow(同步)。

渲染主線程將會使用一套復(fù)雜的策略對整個布局樹進(jìn)行分層。分層的好處在于,將來某一層改變之后,僅會對該層進(jìn)行后續(xù)處理,不影響其他分層,從而提升效率。
首先需要生成繪制的指令,主線程會為每個分層生成繪制指令集,表明如何進(jìn)行繪制,用于描述這一層的內(nèi)容如何畫出來。

移動畫筆到 (x,y) 繪制寬為w,高為h的矩形......
實(shí)際上,canvas是瀏覽器將繪制過程封裝后提供給開發(fā)者的工具。
重繪(repaint) 的本質(zhì)就是重新根據(jù)分層信息計(jì)算了繪制指令。當(dāng)改動可見樣式后,就需要重新計(jì)算繪制指令,引發(fā) Repaint。由于元素的布局(Layout)信息也屬于可見樣式,所以 Reflow 一定會引起 Repaint。

分塊-Tiling
分塊會將每一層分成多個小的區(qū)域。
完成生成繪制指令集之后,主線程會將每個圖層的繪制指令信息提交給合成線程,剩余工作將由合成線程完成。
合成線程首先會對每個圖層進(jìn)行分塊,將其劃分成更多的小區(qū)域。它會從線程池中拿出多個線程來完成分塊工作。Tips: 合成線程和渲染主線程都位于渲染進(jìn)程里。
分塊完成后,會進(jìn)入光柵化階段。合成線程會將塊信息交給GPU進(jìn)程,以極高的速度完成光柵化,GPU會開啟多個線程來完成光柵化,并且優(yōu)先處理靠近視口的塊(類似于懶加載策略,以提高性能)。

光柵化就是將每個塊變成位圖(像素點(diǎn))。

合成線程會計(jì)算出每個位圖在屏幕上的位置,交給GPU進(jìn)行最終的呈現(xiàn)。

合成線程拿到每個層、每個塊的位圖后,生成一個個的quad(指引)信息,指明位圖信息位于屏幕上的位置,以及會考慮到transform的旋轉(zhuǎn)、偏移、縮放等矩陣變換。

如上圖中,為什么合成線程不直接將結(jié)果交給硬件,將內(nèi)容顯示到屏幕上,而要先轉(zhuǎn)交給GPU進(jìn)程,由GPU進(jìn)程轉(zhuǎn)發(fā)呢?
其實(shí)是因?yàn)楹铣删€程和渲染主線程都屬于渲染進(jìn)程,渲染進(jìn)程處于沙盒中,無法進(jìn)行系統(tǒng)調(diào)度,即無法直接與硬件GPU通信,所以需要GPU進(jìn)程中轉(zhuǎn)一下。
總結(jié)
整體的流程如下:

