【瀏覽器】瀏覽器基本工作原理
1.瀏覽器內部組成
我們先來看瀏覽器的內部組成(以chrome為例):
我們看到瀏覽器主要包括:
-
1個瀏覽器主進程
主要負責界面顯示,用戶交互,子進程管理 -
多個渲染進程
一般瀏覽器會為每個Tab標籤窗口創建一個渲染進程,主要負責將html,css,JavaScript轉換成我們看到的網頁,裏面包含多個線程,比如JavaScript的V8引擎。 -
1個GPU進程
主要負責複雜的計算,比如3D動畫,圖形繪製。 -
1個網絡進程
主要負責網絡資源加載 -
多個插件進程
瀏覽器器每個插件都會分配一個插件進程。
2.從一個url開始
我們下面來看在地址欄輸入一個url後,瀏覽器做了什麼事,我們先來看下流程圖:
下面我們來分析下上面的流程圖:
- 當用戶在地址欄輸入一個地址或者關鍵字,並按下回車鍵的時候,意味着當前頁面很快要被替換,在這個時候會觸發當前頁面的beforeunload事件。
然後瀏覽器的當前tab欄就變成加載狀態,變成一個轉動的圓圈,此時頁面還沒有開始改變,需要等到後面「提交文檔」後,才會別新內容替換。 - 瀏覽器主進程合成完整Url:如果是輸入的是地址,比如 “baidu.com”,則自動合成為://www.baidu.com/。
如果輸入的是關鍵字,則使用默認搜索引擎,合成帶搜索關鍵字Url,比如輸入:’hello’,默認搜索引擎為百度,則合成為://www.baidu.com/s?ie=UTF-8&wd=hello
然後把完整url發送給網絡進程。 - 網絡進程接收到url請求後,先判斷是否本地緩存了資源。如果有,則直接返回資源給瀏覽器主進程,不發起網絡請求。如果沒有緩存,則進入網絡請求。
- 網絡請求之前,先要進行DNS解析,把域名轉換成ip,這一步也是先查DNS緩存,如果有當前域名的緩存,則從緩存中直接取對應ip。
如果沒有緩存,則從DMS服務器請求ip。然後構建請求體,請求頭(包括cookie)等信息,向服務端發送網絡請求(建立Tcp鏈接)。 - 服務端接收到請求消息後,進行對應操作,然後生成響應數據,發送給網絡進程。
- 網絡進程接收到服務器返回的響應數據後,先解析響應頭信息,判斷狀態碼是否為重定向(3xx),如果是,則取響應頭中Location字段,重新發起請求。
如狀態碼為200,表示請求成功,可以繼續處理請求。 - 如果狀態碼為200,瀏覽器主進程會根據響應頭中的Content-Type字段做出響應對策,如果此字段的值為application/octet-stream,則啟動下載流程。
如果Content-Type為text/html,則啟動渲染流程。 - 默認情況下,瀏覽器會為每一個tab頁簽創建一個渲染進程,但是如果是同一個站點(根域名+協議相同,端口+子域名不同),則共用一個渲染進程。
- 進入渲染流程開始前,瀏覽器主進程會發送一個「接收文檔」消息給渲染進程,這裡的文檔是指存在網絡進程裏面的響應體信息。
- 渲染進程接收到「提交文檔」的消息後,會和網絡進程建立一個通道,接收數據。
- 渲染進程接收到數據後,開始向瀏覽器主進程發送「確認提交」,消息
- 瀏覽器主進程接收到「確認提交」的消息後,開始更新瀏覽器頁面,包括:地址欄的url,前進後退按鈕。
- 渲染進程開始生成頁面,這個過程是一邊接收一邊生成。當頁面渲染完畢後(當前頁面及內部iframe都出發了onload事件),發送「渲染完畢」消息。
- 瀏覽器主進程接收到消息後,顯示頁面,並停止標籤欄的加載動畫。
到這裡為止,當我們在地址欄輸入一個url,然後到頁面展示在我們面前的大致流程就梳理完畢了。但是這裏面還有一個非常重要的環節,就是頁面解析
的流程我們上面只是一帶而過,這是渲染進程來做的工作,下面來具體展開。
3.渲染進程
渲染進程的核心工作就是解析接收到的html/js/css代碼,並將其轉換成用戶可交互的頁面。
渲染進成包含:
- 主線程GUI:負責解析dom結構
- js引擎線程:負責執行js代碼,會阻塞主進程。
- 合成線程:分組,合成,並把視口附近圖塊提交給光柵化線程。
- 多個光柵化線程:生成位圖,即頁面需要的每個像素點的顏色值(我們看到的頁面其實就是每個像素點的顏色)
下面來分析以下流程圖: - 渲染進程開始接受到數據的時候,為了提高效率,會先預掃描接收到的數據,如果如果發現有需要加載資源的標籤(img,link,外部script等),就先告訴瀏覽器主進程,先去下載,這個過程叫
預解析
,這個任務交出去後,就繼續做自己本職工作,解析html文件。
-當主線程解析html
文件時,會碰到三種類型數據:html標籤,css代碼,js代碼。- html標籤:對於普通的html標籤,會
生成Dom樹
(標籤節點的結構樹,是瀏覽器的內置對象,會有一些內置方法和屬性)。 - css樣式:對於css代碼,會根據css的樣式選擇器
構建cssDom樹
,並對樣式進行計算(rem,em轉換為px,沒有定義樣式的提供默認樣式),生成computedStyle
。
如果遇到的是css外部鏈接,如果從預解析開始還沒下載完,則繼續下載,不會阻塞解析。 - js代碼:對於js代碼,會先判斷js代碼前的css有沒有解析完(包括外部css的下載),如果沒有則等待css代碼下載完並解析完畢,然後再執行js代碼。js執行期間
阻塞解析
。所以步驟是這樣:
遇到js -> 阻塞dom樹構建 -> css下載 -> css解析->js執行->繼續構建dom樹 - js鏈接:對於js的鏈接,如果標籤上沒有設置異步標誌(async/defer),則和普通的js代碼一樣,下載也會阻塞dom解析,也需要等css下載解析完,但是css下載不會阻塞js下載,步驟如下:
遇到js鏈接(無異步標籤) -> 阻塞dom樹構建 -> css下載(同時js下載) -> css解析->js執行->繼續構建dom樹
如果有異步標籤,則下載不阻塞dom樹構建,async文件下載完,立即執行。defer文件下載完,等html解析完,按加載順序執行。步驟如下:
遇到js鏈接(async) ->下載js(不影響dom構建) -> js下載完畢 -> 立即執行js(走普通js代碼流程)
遇到js鏈接(defer) ->下載js(不影響dom構建) -> js下載完畢 -> 等html解析完畢 -> 按順序執行js
- html標籤:對於普通的html標籤,會
- 等dom樹和computedStyle都構建完畢後(要都構建完畢), 更具dom樹和computedStyle,
構建布局樹layoutTree
,布局樹包含每個節點的位置坐標
和盒模型的大小
,並且剔除了隱藏
的節點(樣式設置了display:none的節點)。 - 等布局樹layoutTree構建完畢後,我們已經知道了頁面上要顯示的每個節點的大小,位置和樣式。繼續來主線程會對節點進行分層,通過遍歷layoutTree
構建圖層樹layerTree
。哪些節點會被分為一層呢?分為兩種情況:- 擁有層疊上下文屬性的元素會被單獨提升為一層(什麼是層疊上下文),包含設置了z-index,transform,will-change,filter,opacity<1,flex子元素等等。
- 需要裁剪的地方會被分為一層,即元素的大小被限制,而內容超出元素大小,內容被裁剪。
- 圖層樹layerTree被創建後,會為每一個圖層
創建繪製指令
列表,可以再瀏覽器調試窗口的layers標籤下查看分層和指令列表信息。渲染進程的主線程把繪製指令生成後,並不執行,而是轉交給合成線程。 - 合成線程先把
圖層分為圖塊
(大小通常為256256/512512),然後把瀏覽器用戶視口附近
的圖塊優先交給柵格化線程來生成位圖。 - 柵格化的最小執行單位是圖塊,即最少要把一個圖塊
柵格化
。柵格化的過程通常會用GPU執行,就是說柵格化線程會把繪製圖塊的指令發送給GPU,然後GPU生成圖塊的位圖(像素點的顏色值),存在GPU內存。 - 當視口附近所有圖塊柵格化完畢後,合成線程發送DrawQuad指令給瀏覽器主進程,瀏覽器主進程把頁面的內容顯示在屏幕上。
4.應用
那麼知道了瀏覽器的基本原理後,對我們開發有什麼實際的作用呢?以下總結了幾點:
- css會阻塞js,js會阻塞dom解析,所以盡量把css文件放頁面上面,js放在頁面下面。
- 對於不會影響頁面內容的js外部文件,可以用async/defer標記來異步加載。
- css動畫效率比js操作dom實現動畫好,因為css動畫的只會引起
合成
及以後步驟的重新執行。而合成步驟是在合成線程
,不會阻塞渲染的主線程。而js如果影響到dom節點的大小樣式位置,則需要觸發布局
及以後的步驟。
參考: