完整高頻題庫(kù)倉(cāng)庫(kù)地址:https://github.com/hzfe/awesome-interview
完整高頻題庫(kù)閱讀地址:https://febook.hzfe.org/
相關(guān)問(wèn)題
- 瀏覽器如何渲染頁(yè)面
- 有哪些提高瀏覽器渲染性能的方法
回答關(guān)鍵點(diǎn)
DOM CSSOM 線程互斥 渲染樹 Compositing GPU 加速
當(dāng)瀏覽器進(jìn)程獲取到 HTML 的第一個(gè)字節(jié)開始,會(huì)通知渲染進(jìn)程開始解析 HTML,將 HTML 轉(zhuǎn)換成 DOM 樹,并進(jìn)入渲染流程。一般所有的瀏覽器都會(huì)經(jīng)過(guò)五大步驟,分別是:
- PARSE:解析 HTML,構(gòu)建 DOM 樹。
- STYLE:為每個(gè)節(jié)點(diǎn)計(jì)算最終的有效樣式。
- LAYOUT:為每個(gè)節(jié)點(diǎn)計(jì)算位置和大小等布局信息。
- PAINT:繪制不同的盒子,為了避免不必要的重繪,將會(huì)分成多個(gè)層進(jìn)行處理。
- COMPOSITE & RENDER:將上述不同的層合成為一張位圖,發(fā)送給 GPU,渲染到屏幕上。
為了提高瀏覽器的渲染性能,通常的手段是保證渲染流程不被阻塞,避免不必要的繪制計(jì)算和重排重繪,利用 GPU 硬件加速等技術(shù)來(lái)提高渲染性能。
知識(shí)點(diǎn)深入
1. 瀏覽器的渲染流程
Chromium 的渲染流程的主要步驟如下圖所示:

圖片來(lái)源 Life of a Pixel
1.1 Parse 階段:解析 HTML
構(gòu)建 DOM 樹
渲染進(jìn)程主線程解析 HTML 并構(gòu)建出結(jié)構(gòu)化的樹狀數(shù)據(jù)結(jié)構(gòu) DOM 樹,需要經(jīng)歷以下幾個(gè)步驟:
- Conversion(轉(zhuǎn)換):瀏覽器從網(wǎng)絡(luò)或磁盤讀取 HTML 文件原始字節(jié),根據(jù)指定的文件編碼(如 UTF-8)將字節(jié)轉(zhuǎn)換成字符。
- Tokenizing(分詞):瀏覽器根據(jù) HTML 規(guī)范將字符串轉(zhuǎn)換為不同的標(biāo)記(如
<html>,<body>)。 - Lexing(語(yǔ)法分析):上一步產(chǎn)生的標(biāo)記將被轉(zhuǎn)換為對(duì)象,這些對(duì)象包含了 HTML 語(yǔ)法的各種信息,如屬性、屬性值、文本等。
- DOM construction(DOM 構(gòu)造):因?yàn)?HTML 標(biāo)記定義了不同標(biāo)簽之間的關(guān)系,上一步產(chǎn)生的對(duì)象會(huì)鏈接在一個(gè)樹狀數(shù)據(jù)結(jié)構(gòu)中,以標(biāo)識(shí)父子、兄弟關(guān)系。
構(gòu)建 DOM 的流程如下圖所示:

圖片來(lái)源 Constructing the Object Model
次級(jí)資源加載
一個(gè)網(wǎng)頁(yè)通常會(huì)使用多個(gè)外部資源,如圖片、JavaScript、CSS、字體等。主線程在解析 DOM 的過(guò)程中遇到這些資源后會(huì)一一請(qǐng)求。為了加速渲染流程,會(huì)有一個(gè)叫做預(yù)加載掃描器(preload scanner)線程并發(fā)運(yùn)行。如果 HTML 中存在 img 或 link 之類的內(nèi)容,則預(yù)加載掃描器會(huì)查看 HTML parser 生成的標(biāo)記,并發(fā)送請(qǐng)求到瀏覽器進(jìn)程的網(wǎng)絡(luò)線程獲取這些資源。
JavaScript 可能阻塞解析
當(dāng) HTML 解析器發(fā)現(xiàn) script 標(biāo)簽時(shí),會(huì)暫停 HTML 的解析,轉(zhuǎn)而開始加載、解析和執(zhí)行 JavaScript。因?yàn)?JS 可能會(huì)改變 DOM 的結(jié)構(gòu)。如果不想因 JS 阻塞 HTML 的解析,可以為 script 標(biāo)簽添加 defer 屬性或?qū)?script 放在 body 結(jié)束標(biāo)簽之前,瀏覽器會(huì)在最后執(zhí)行 JS 代碼,避免阻塞 DOM 構(gòu)建。
1.2 Style 階段:樣式計(jì)算
CSS 引擎處理樣式的過(guò)程分為三個(gè)階段:
- 收集、劃分和索引所有樣式表中存在的樣式規(guī)則,CSS 引擎會(huì)從 style 標(biāo)簽,css 文件及瀏覽器代理樣式中收集所有的樣式規(guī)則,并為這些規(guī)則建立索引,以方便后續(xù)的高效查詢。
- 訪問(wèn)每個(gè)元素并找到適用于該元素的所有規(guī)則,CSS 引擎遍歷 DOM 節(jié)點(diǎn),進(jìn)行選擇器匹配,并為匹配的節(jié)點(diǎn)執(zhí)行樣式設(shè)置。
- 結(jié)合層疊規(guī)則和其他信息為節(jié)點(diǎn)生成最終的計(jì)算樣式,這些樣式的值可以通過(guò)
window.getComputedStyle()獲取。
在大型網(wǎng)站中,會(huì)存在大量的 CSS 規(guī)則,如果為每個(gè)節(jié)點(diǎn)都保存一份樣式值,會(huì)導(dǎo)致內(nèi)存消耗過(guò)大。作為替代,CSS 引擎通常會(huì)創(chuàng)建共享的樣式結(jié)構(gòu),計(jì)算樣式對(duì)象一般有指針指向相同的共享結(jié)構(gòu)。
附加了計(jì)算樣式的 DOM 樹,一般被稱為 CSSOM(CSS Object Model):

圖片來(lái)源 Constructing the Object Model
CSSOM 和 DOM 是并行構(gòu)建的,構(gòu)建 CSSOM 不會(huì)阻塞 DOM 的構(gòu)建。但 CSSOM 會(huì)阻塞 JS 的執(zhí)行,因?yàn)?JS 可能會(huì)操作樣式信息。雖然 CSSOM 不會(huì)阻塞 DOM 的構(gòu)建,但在進(jìn)入下一階段之前,必須等待 CSSOM 構(gòu)建完成。這也是通常所說(shuō)的 CSSOM 會(huì)阻塞渲染。
1.3 Layout 階段
創(chuàng)建 LayoutObject(RenderObject) 樹
有了 DOM 樹和 DOM 樹中元素的計(jì)算樣式后,瀏覽器會(huì)根據(jù)這些信息合并成一個(gè) layout 樹,收集所有可見(jiàn)的 DOM 節(jié)點(diǎn),以及每個(gè)節(jié)點(diǎn)的所有樣式信息。
Layout 樹和 DOM 樹不一定是一一對(duì)應(yīng)的,為了構(gòu)建 Layout 樹,瀏覽器主要完成了下列工作:
- 從 DOM 樹的根節(jié)點(diǎn)開始遍歷每個(gè)可見(jiàn)節(jié)點(diǎn)。
- 某些不可見(jiàn)節(jié)點(diǎn)(例如 script、head、meta 等),它們不會(huì)體現(xiàn)在渲染輸出中,會(huì)被忽略。
- 某些通過(guò)設(shè)置 display 為 none 隱藏的節(jié)點(diǎn),在渲染樹中也會(huì)被忽略。
- 為偽元素創(chuàng)建 LayoutObject。
- 為行內(nèi)元素創(chuàng)建匿名包含塊對(duì)應(yīng)的 LayoutObject。

圖片來(lái)源 Render-tree Construction
布局計(jì)算
上一步計(jì)算了可見(jiàn)的節(jié)點(diǎn)及其樣式,接下來(lái)需要計(jì)算它們?cè)谠O(shè)備視口內(nèi)的確切位置和大小,這個(gè)過(guò)程一般被稱為自動(dòng)重排。
瀏覽器的布局計(jì)算工作包含以下內(nèi)容:
- 根據(jù) CSS 盒模型及視覺(jué)格式化模型,計(jì)算每個(gè)元素的各種生成盒的大小和位置。
- 計(jì)算塊級(jí)元素、行內(nèi)元素、浮動(dòng)元素、各種定位元素的大小和位置。
- 計(jì)算文字,滾動(dòng)區(qū)域的大小和位置。
- LayoutObject 有兩種類型:
- 傳統(tǒng)的 LayoutObject 節(jié)點(diǎn),會(huì)把布局運(yùn)算的結(jié)果重新寫回布局樹中。
- LayoutNG(Chrome 76 開始啟用) 節(jié)點(diǎn)的輸出是不可變的,會(huì)保存在 NGLayoutResult 中,這是一個(gè)樹狀的結(jié)構(gòu),相比之前的 LayoutObject,少了很大回溯計(jì)算,提高了性能。
1.4 Paint 階段
Paint 階段將 LayoutObject 樹轉(zhuǎn)換成供合成器使用的高效渲染格式,包括一個(gè)包含 display item 列表的 cc::Layers 列表,與該列表與 cc::PropertyTrees 關(guān)聯(lián)。
構(gòu)建 PaintLayer(RenderLayer) 樹
構(gòu)建完成的 LayoutObject 樹還不能拿去顯示,因?yàn)樗话L制的順序(z-index)。同時(shí),也為了考慮一些復(fù)雜的情況,如 3D 變換、頁(yè)面滾動(dòng)等,瀏覽器會(huì)對(duì)上一步的節(jié)點(diǎn)進(jìn)行分層處理。這個(gè)處理過(guò)程被稱為建立層疊上下文。
瀏覽器會(huì)根據(jù) CSS 層疊上下文規(guī)范,建立層疊上下文,常見(jiàn)情況如下:
- DOM 樹的 Document 節(jié)點(diǎn)對(duì)應(yīng)的 RenderView 節(jié)點(diǎn)。
- DOM 樹中 Document 節(jié)點(diǎn)的子節(jié)點(diǎn),也就是 HTML 節(jié)點(diǎn)對(duì)應(yīng)的 RenderBlock 節(jié)點(diǎn)。
- 顯式指定 CSS 位置的節(jié)點(diǎn)(position 為 absolute 或者 fixed)。
- 具有透明效果的節(jié)點(diǎn)。
- 具有 CSS 3D 屬性的節(jié)點(diǎn)。
- 使用 Canvas 元素或者 Video 元素的節(jié)點(diǎn)。
瀏覽器遍歷 LayoutObject 樹的時(shí)候,建立了 PaintLayer 樹,LayoutObject 與 PaintLayer 也不一定是一一對(duì)應(yīng)的。每個(gè) LayoutObject 要么與自己的 PaintLayer 關(guān)聯(lián),要么與擁有 PaintLayer 的第一個(gè)祖先的 PaintLayer 關(guān)聯(lián)。
構(gòu)建 cc::Layer 與 display items
瀏覽器會(huì)繼續(xù)根據(jù) PaintLayer 樹創(chuàng)建 cc::Layer 列表。cc::Layer 是列表狀結(jié)構(gòu),每個(gè) layer 包含了個(gè) DisplayItem 列表,每個(gè) DisplayItem 包含了實(shí)際的 paint op 指令。將頁(yè)面分層,可以讓一個(gè)圖層獨(dú)立于其他的圖層進(jìn)行變換和光柵化處理。
合成更新(Compositing update)
- 依據(jù) PaintLayer 決定分層(GraphicsLayers)
- 這個(gè)策略被稱為 CompositeBeforePaint,未來(lái)會(huì)被 CompositeAfterPaint 替代。
PrePaint
- PaintInvalidator 進(jìn)行失效檢查,找出需要繪制的 display items。
- 構(gòu)建 paint property 樹,該樹能使動(dòng)畫、頁(yè)面滾動(dòng),clip 等變化僅在合成線程運(yùn)行,提高性能。
Paint
- 遍歷 LayoutObject 樹并創(chuàng)建 display items 列表。
- 為共享同樣 property tree 狀態(tài)的 display items 列表創(chuàng)建 paint chunks 分組。
- 將結(jié)果 commit 到 compositor。
- CompositeAfterPaint 將在此時(shí)決定分層。
- 將 paint chunks 通過(guò) cc::Layer 列表傳遞給 compositor。
- 將 property 樹轉(zhuǎn)換為 cc::PropertyTrees。
上面的流程中,有兩個(gè)不同的創(chuàng)建合成層的時(shí)機(jī),一個(gè)是 paint 之前的 CompositeBeforePaint,該操作在渲染主線程中完成。一個(gè)是 paint 之后的 CompositeAfterPaint,后續(xù)創(chuàng)建 layer 的操作在 CC(Chromium Compositor)線程中完成。
1.5 合成 Compositing
合成階段在 CC(Chromium Compositor)線程中進(jìn)行。
commit
當(dāng) Paint 階段完成后,主線程進(jìn)入 commit 階段,將 cc::Layer 中的 layer list 和 property 樹更新到 CC 線程的 LayerImpl 中,commit 完成。commit 進(jìn)行的過(guò)程中,主線程被阻塞。
tiling & raster
raster(光柵化)是將 display item 中的繪制操作轉(zhuǎn)換為位圖的過(guò)程。
光柵化的主要操作流程如下:
- tiling:將 layer 分成 tiles(圖塊)。因?yàn)橛械?layer 可能很大(如整個(gè)文檔的滾動(dòng)根節(jié)點(diǎn)),對(duì)整層的光柵化操作代價(jià)昂貴,且 layer 中有的部分是不可見(jiàn)的,會(huì)造成不必要的浪費(fèi)。
- tiles 是光柵化的基本單元。光柵化操作是通過(guò)光柵線程池處理的。離視口更近的 tiles 具有更高的優(yōu)先級(jí),將優(yōu)先處理。
- 一個(gè) layer 實(shí)際上會(huì)生成多種分辨率的 tiles。
- raster 同樣也會(huì)處理頁(yè)面引用的圖片資源,display items 中的 paint ops 引用了這些壓縮數(shù)據(jù),raster 會(huì)調(diào)用合適的解碼器來(lái)解壓這些數(shù)據(jù)。
- raster 會(huì)通過(guò) Skia 來(lái)進(jìn)行 OpenGL 調(diào)用,光柵化數(shù)據(jù)。
- 渲染進(jìn)程是運(yùn)行在沙箱中的,不能直接進(jìn)行系統(tǒng)調(diào)用。paint ops 通過(guò) IPC(MOJO)傳遞給 GPU 進(jìn)程,GPU 進(jìn)程會(huì)執(zhí)行真實(shí)的 OpenGL(為了保證性能,在 Windows 上轉(zhuǎn)為 DirectX)調(diào)用。
- 光柵化的位圖結(jié)果保存在 GPU 內(nèi)存中,通常作為 OpenGL 材質(zhì)對(duì)象保存。
- 雙緩沖機(jī)制:主線程隨時(shí)會(huì)有 commit 到來(lái),當(dāng)前的光柵化行為在 pending tree(LayerImpl)上進(jìn)行,一旦光柵化操作完成,將 pending tree 變?yōu)?active tree,后續(xù)的 draw 操作在 active tree 上進(jìn)行。
draw
當(dāng)所有的 tiles 都完成光柵化后,會(huì)生成 draw quads(繪制四邊形)。每個(gè) draw quads 是包含一個(gè)在屏幕特定位置繪制 tile 的命令,該命令同時(shí)考慮了所有應(yīng)用到 layer tree 的變換。每個(gè)四邊形引用了內(nèi)存中 tile 的光柵化輸出。四邊形被包裹在合成幀對(duì)象(compositor frame object)中,然后提交(submit)到瀏覽器進(jìn)程。
display compositor(viz,visual 的簡(jiǎn)稱)
viz 位于 GPU 進(jìn)程中,viz 接收來(lái)自瀏覽器的合成幀,合成幀來(lái)自多個(gè)渲染進(jìn)程,以及瀏覽器自身 UI 的 compositor。
合成幀和屏幕上將要繪制的位置關(guān)聯(lián),該位置叫做 surface。surface 可以嵌套其他 surface,瀏覽器 UI 的 surface 嵌套了渲染進(jìn)程的 surface,渲染進(jìn)程的 surface 嵌套了其他跨域 iframes(同源的 iframe 共享相同的渲染進(jìn)程) 的 surface。viz 同步傳入的幀,并處理嵌套 surfaces 的依賴(surface aggregation)。
最終的顯示流程:
- viz 會(huì)發(fā)出 OpenGL 調(diào)用將合成幀中的 quads 發(fā)送到 GPU 線程的 backbuffer 中。
- 在新的模式中,viz 會(huì)使用 Skia 代替原始 OpenGL 調(diào)用。
- 在大部分平臺(tái)上,viz 的輸出也是雙緩沖結(jié)構(gòu),draw 首先到達(dá) backbuffer,通過(guò) swapping 操作轉(zhuǎn)換成 frontbuffer 最終顯示在屏幕上。
線程對(duì)瀏覽器事件的處理
合成的優(yōu)點(diǎn)是它在不涉及渲染主線程的情況下完成的。合成器不需要等待樣式計(jì)算或 JavaScript 執(zhí)行。只和合成相關(guān)的動(dòng)畫被認(rèn)為是獲得流暢性能的最佳選擇。同時(shí),合成器還負(fù)責(zé)處理頁(yè)面的滾動(dòng),滾動(dòng)的時(shí)候,合成器會(huì)更新頁(yè)面的位置,并且更新頁(yè)面的內(nèi)容。
當(dāng)一個(gè)沒(méi)有綁定任何事件的頁(yè)面發(fā)生滾動(dòng)時(shí),合成器可以獨(dú)立于渲染主線程之外進(jìn)行合成幀的的創(chuàng)建,保證頁(yè)面的流程滾動(dòng)。當(dāng)頁(yè)面中的某一區(qū)域綁定了 JS 事件處理程序時(shí),CC 線程會(huì)將這一區(qū)域標(biāo)記為 Non-Fast Scrollable Region。如果事件來(lái)自于該區(qū)域之外,則 CC 線程繼續(xù)合成新的幀,而無(wú)需等待主線程。
在開發(fā)中,我們通常會(huì)使用事件委托來(lái)簡(jiǎn)化邏輯,但是這會(huì)使整個(gè)綁定事件的區(qū)域變成 Non-Fast Scrollable Region。為了減輕這種情況對(duì)滾動(dòng)造成的影響,你可以傳入 passive: true 選項(xiàng)到事件監(jiān)聽(tīng)器中。
document.body.addEventListener(
??"touchstart",
??(event)?=>?{
????if?(event.target?===?area)?{
??????event.preventDefault();
????}
??},
??{?passive:?true?}
);2. 瀏覽器渲染性能的優(yōu)化
上一節(jié)中是一輪典型的瀏覽器渲染流程,在流程完成之后,DOM、CSSOM、LayoutObject、PaintLayer 等各種樹狀數(shù)據(jù)結(jié)構(gòu)都會(huì)保留下來(lái),以便在用戶操作、網(wǎng)絡(luò)請(qǐng)求、JS 執(zhí)行等事件發(fā)生時(shí),重新觸發(fā)渲染流程。
2.1 減少渲染中的重排重繪
瀏覽器重新渲染時(shí),可能會(huì)從中間的任一步驟開始,直至渲染完成。因此,盡可能的縮短渲染路徑,就可以獲得更好的渲染性能。當(dāng)瀏覽器重新繪制一幀的時(shí)候,一般需要經(jīng)過(guò)布局、繪圖和合成三個(gè)主要階段。這三個(gè)階段中,計(jì)算布局和繪圖比較費(fèi)時(shí)間,而合成需要的時(shí)間相對(duì)少一些。
以動(dòng)畫為例,如果使用 JS 的定時(shí)器來(lái)控制動(dòng)畫,可能就需要較多的修改布局和繪圖的操作,一般有以下兩種方法進(jìn)行優(yōu)化:
- 使用合適的網(wǎng)頁(yè)分層技術(shù):如使用多層 canvas,將動(dòng)畫背景,運(yùn)動(dòng)主體,次要物體分層,這樣每一幀需要變化的就只是一個(gè)或部分合成層,而不是整個(gè)頁(yè)面。
- 使用 CSS Transforms 和 Animations:它可以讓瀏覽器僅僅使用合成器來(lái)合成所有的層就可以達(dá)到動(dòng)畫效果,而不需要重新計(jì)算布局,重新繪制圖形。CSS Triggers 中僅觸發(fā) Composite 的屬性就是最優(yōu)的選擇。
2.2 優(yōu)化影響渲染的資源
在瀏覽器解析 HTML 的過(guò)程中,CSS 和 JS 都有可能對(duì)頁(yè)面的渲染造成影響。優(yōu)化方法包括以下幾點(diǎn):
- 關(guān)鍵 CSS 資源放在頭部加載。
- JS 通常放在頁(yè)面底部。
- 為 JS 添加 async 和 defer 屬性。
- body 中盡量不要出現(xiàn) CSS 和 JS。
- 為 img 指定寬高,避免圖像加載完成后觸發(fā)重排。
- 避免使用 table, iframe 等慢元素。原因是 table 會(huì)等到它的 dom 樹全部生成后再一次性插入頁(yè)面中;iframe 內(nèi)資源的下載過(guò)程會(huì)阻塞父頁(yè)面靜態(tài)資源的下載及 css, dom 樹的解析。

圖片來(lái)源 The Script Element
參考資料
- 瀏覽器的工作原理:新式網(wǎng)絡(luò)瀏覽器幕后揭秘
- 渲染頁(yè)面:瀏覽器的工作原理
- Constructing the Object Model
- Inside a super fast CSS engine
- Render-tree Construction, Layout, and Paint
- Inside look at modern web browser(part 3)
- Inside look at modern web browser(part 4)
- DOM
- CSS
- Layout
- Paint
- how cc works
- Life of a Pixel
