瀏覽器渲染原理
前言
瀏覽器的主要功能總結起來就是一句話:將使用者輸入的 URL
轉變成視覺化的影像。
- 從
URL
到DOM
樹; - 從
DOM
樹到視覺化影像; - 這兩個過程之間的關係並沒有那麼明確,我們可以統稱這兩個過程為頁面的渲染;
1. 網頁的解析過程
- 思考一下:一個網頁從
url
輸入到瀏覽器,經歷過怎樣的解析過程?
- 要想深入理解下載的過程,我們還要先理解,一個
index.html
被下載下來後是如何被解析和顯示在瀏覽器上的;
2. 瀏覽器的功能與組成
2.1 瀏覽器核心
Trident
( 三叉戟): IE、 360 安全瀏覽器、 UC 瀏覽器Gecko
( 壁虎) : Mozilla FirefoxPresto
(急板樂曲) -> Blink (眨眼): OperaWebkit
: Safari、 360 極速瀏覽器、搜狗高速瀏覽器、移動端瀏覽器(Android、 iOS)Webkit
->Blink
: Google Chrome, Edge
瀏覽器的核心相當於汽車的發動機,是最核心的存在,它負責將程式碼轉換成使用者眼中的介面。
我們經常說的瀏覽器核心指的是瀏覽器的排版引擎:
- 排版引擎 (layout engine),也稱為瀏覽器引擎 (browser engine)、頁面渲染引擎 (rendering engine) 或樣版引擎;
- 也就是一個網頁下載下來後,就是由我們的渲染引擎來幫助我們解析的;
2.2 程式與執行緒
程式與執行緒的解釋:
- 程式:程式的一次執行,它佔有一片獨有的記憶體空間,是作業系統執行的基本單元;
- 執行緒:是程式內的一個獨立執行單元,是 CPU 排程的最小單元;
瀏覽器: 多程式、多執行緒模型
Browser
程式:- 瀏覽器的主程式,負責瀏覽器介面的顯示,和各個頁面的管理;
- 瀏覽器中所有其他型別程式的祖先,負責其他程式的的建立和銷燬,它有且只有一個!
Renderer
程式:- 網頁渲染程式,負責頁面的渲染,可以有多個渲染程式的數量,不一定是開啟頁面的數量;
GPU Process
:負責 GPU 相關;Plugin Process
:負責控制一個網頁用到的Plugin
;
3. 瀏覽器渲染流程
3.1 渲染引擎解析過程
渲染引擎在拿到一個頁面後,如何解析整個頁面並且最終呈現出我們的網頁呢?
- 我們透過下面的這幅圖,讓我們更加詳細的學習它的過程;
3.2 渲染引擎主要模組
一個渲染引擎主要包括:
HTML
解析器,CSS
解析器,javascript
引擎,佈局layout
模組,繪圖模組;HTML
解析器:解釋 HTML 文件的解析器,主要作用是將 HTML 文字解釋成 DOM 樹;CSS
解析器:它的作用是為 DOM 中的各個元素物件計算出樣式資訊,為佈局提供基礎設施;Javascript
引擎:使用 Javascript 程式碼可以修改網頁的內容,也能修改 css 的資訊,javascript 引擎能夠解釋 javascript 程式碼,並透過 DOM 介面和 CSS 樹介面來修改網頁內容和樣式資訊,從而改變渲染的結果- 佈局 (
layout
):在 DOM 建立之後,Webkit 需要將其中的元素物件同樣式資訊結合起來,計算他們的大小位置等佈局資訊,形成一個能表達這所有資訊的內部表示模型 - 繪圖模組 (
paint
):使用圖形庫將佈局計算後的各個網頁的節點繪製成影像結果
4. 渲染頁面的詳細流程
瀏覽器渲染頁面的整個過程:瀏覽器會從上到下解析文件。
4.1 HTML 解析過程
遇見HTML
標記,呼叫HTML
解析器解析為對應的token
(一個token
就是一個標籤文字的序列化)並構建DOM
樹(就是一塊記憶體,儲存著tokens
,建立它們之間的關係)
4.2 生成 CSS 規則
遇見style/link
標記呼叫解析器處理CSS
標記並構建CSS
樣式樹。
4.3 構建 Render Tree
當有了DOM Tree
和CSSOM Tree
後,就可以兩個結合來構建Render Tree
了
注意一:
link
元素不會阻塞DOM Tree
的構建過程,但是會阻塞Render Tree
的構建過程- 這是因為
Render Tree
在構建時,需要對應的CSSOM Tree
;
- 這是因為
注意二:
Render Tree
和DOM Tree
並不是一一對應的關係- 比如對於
display
為none
的元素,壓根不會出現在render tree
中;
- 比如對於
4.4 佈局 (layout) 和繪製 (Paint)
第四步是:在渲染樹(Render Tree)上執行佈局(Layout) 來計算每個節點的幾何體。
- 渲染樹會表示顯示哪些節點以及其他樣式,但是不表示每個節點的尺寸、位置等資訊;
- 佈局是確定呈現樹中所有節點的寬度、高度和位置資訊;
第五步是:將每個節點繪製(Paint)到螢幕上。
- 在繪製階段,瀏覽器將佈局階段計算的每個 frame 轉為螢幕上實際的畫素點;
- 包括將元素的可見部分進行繪製,比如文字、顏色、邊框、陰影、替換元素(比如 img)
5. 重繪和迴流解析
5.1 重繪(Repaint)
第一次渲染內容稱之為繪製(paint)
- 之後重新渲染稱之為重繪
重繪不會帶來重新佈局,所以並不一定伴隨重排。
- 瀏覽器會根據元素的新屬性重新繪製,使元素呈現新的外觀。
什麼屬性會導致重繪呢?
color background box-shadow..
(比如修改背景色、文字顏色、邊框顏色、樣式等)
5.2 迴流/重排(reflow)
理解迴流 reflow
:也可以稱之為重排
第一次確定節點的大小和位置,稱之為佈局(layout)
- 之後對節點的大小、位置修改重新計算稱之為迴流
"重排/迴流"必然導致"重繪"
- 比如改變一個網頁元素的位置,就會同時觸發"重排"和"重繪",因為佈局改變了
- 所以迴流是一件很消耗效能的事情
什麼屬性會導致迴流呢?
width top position..
(比如 DOM 結構發生改變 / 修改了佈局)
5.3 常見的觸發"迴流/重排"操作
- Reflow 的成本比 Repaint 的成本高得多的多。
所以在開發中要儘量避免發生迴流:
修改樣式時儘量一次性修改
- 將多次改變樣式屬性的操作合併成一次操作
- 預先定義好 class,然後修改 DOM 的 className
儘量避免頻繁的操作
DOM
- 我們可以在一個
DocumentFragment
或者父元素中將要操作的DOM
操作完成,再一次性的操作;
- 我們可以在一個
- 儘量避免透過
getComputedStyle
獲取尺寸、位置等資訊; 對某些元素使用
position
的absolute
或者fixed
- 並不是不會引起迴流,而是開銷相對較小,不會對其他元素造成影響。
5.4 合成和效能最佳化
繪製的過程,可以將佈局後的元素繪製到多個合成圖層中
- 這是瀏覽器的一種最佳化手段
- 預設情況下,標準流中的內容都是被繪製在同一個圖層(Layer)中的
而一些特殊的屬性,會建立一個新的合成層( CompositingLayer ),並且新的圖層可以利用 GPU 來加速繪製
- 因為每個合成層都是單獨渲染的
那麼哪些屬性可以形成新的合成層呢? 常見的一些屬性:
- 3D transforms
- video、 canvas、 iframe
- opacity 動畫轉換時
- position: fixed
- will-change:一個實驗性的屬性,提前告訴瀏覽器元素可能發生哪些變化
- animation 或 transition 設定了 opacity、 transform
- 分層確實可以提高效能,但是它以記憶體管理為代價,因此不應作為 web 效能最佳化策略的一部分過度使用
5.5 script 元素和頁面解析的關係
我們現在已經知道了頁面的渲染過程,但是
JavaScript
在哪裡呢?- 事實上,瀏覽器在解析
HTML
的過程中,遇到了script
元素是不能繼續構建DOM
樹的; - 它會停止繼續構建,首先下載
JavaScript
程式碼,並且執行JavaScript
的指令碼; - 只有等到
JavaScript
指令碼執行結束後,才會繼續解析HTML
,構建DOM
樹;
- 事實上,瀏覽器在解析
為什麼要這樣做呢?
- 這是因為
JavaScript
的作用之一就是操作DOM
,並且可以修改DOM
; - 如果我們等到
DOM
樹構建完成並且渲染再執行JavaScript
,會造成嚴重的迴流和重繪,影響頁面的效能; - 所以會在遇到
script
元素時,優先下載和執行JavaScript
程式碼,再繼續構建DOM
樹;
- 這是因為
但是這個也往往會帶來新的問題,特別是現代頁面開發中:
- 在目前的開發模式中(比如
Vue
、React
),指令碼往往比HTML
頁面更“重”,處理時間需要更長; - 所以會造成頁面的解析阻塞,在指令碼下載、執行完成之前,使用者在介面上什麼都看不到;
- 在目前的開發模式中(比如
- 為了解決這個問題,
script
元素給我們提供了兩個屬性(attribute
):defer
和async
。
6. defer 和 async 屬性
6.1 defer 屬性
defer
屬性:告訴瀏覽器不要等待指令碼下載,而繼續解析 HTML,構建 DOM Tree。- 指令碼會由瀏覽器來進行下載,但是不會阻塞
DOM Tree
的構建過程; - 如果指令碼提前下載好了,它會等待 DOM Tree 構建完成,在 DOMContentLoaded 事件之前先執行 defer 中的程式碼;
- 指令碼會由瀏覽器來進行下載,但是不會阻塞
- 所以 DOMContentLoaded 總是會等待 defer 中的程式碼先執行完成。
另外多個帶
defer
的指令碼是可以保持正確的順序執行的。- 從某種角度來說,
defer
可以提高頁面的效能,並且推薦放到head
元素中; - 注意:
defer
僅適用於外部指令碼,對於script
預設內容會被忽略。
- 從某種角度來說,
6.2 async 屬性
sync
特性與defer
有些類似,它也能夠讓指令碼不阻塞頁面。async 是讓一個指令碼完全獨立的:
- 瀏覽器不會因
async
指令碼而阻塞(與defer
類似); async
指令碼不能保證順序,它是獨立下載、獨立執行,不會等待其他指令碼;- async 不會能保證在 DOMContentLoaded 之前或者之後執行
- 瀏覽器不會因
defer
通常用於需要在文件解析後操作 DOM 的 JavaScript 程式碼,並且對多個 script 檔案有順序要求的;async
通常用於獨立的指令碼,對其他指令碼,甚至 DOM 沒有依賴的;