瀏覽器中的 HTTP 請求從發起到結束一共經歷了八個階段:構建請求、查找緩存、準備 IP 和端口、等待 TCP 隊列、建立 TCP 連接、發起 HTTP 請求、服務器處理請求、服務器返回請求和斷開連接。
三、導航流程
在瀏覽器里,從輸入 URL 到頁面展示,這中間發生了什么?其中涉及到了網絡、操作系統、Web 等一系列的知識,如下圖所示。
整個流程大致描述如下:
(1)首先,瀏覽器進程接收到用戶輸入的 URL 請求,瀏覽器進程便將該 URL 轉發給網絡進程。
(2)然后,在網絡進程中發起真正的 URL 請求。
(3)接著,網絡進程接收到了響應頭數據,便解析響應頭數據,并將數據轉發給瀏覽器進程。
(4)瀏覽器進程接收到網絡進程的響應頭數據之后,發送“提交導航 ( )”消息到渲染進程;
(5)渲染進程接收到“提交導航”的消息之后,便開始準備接收 HTML 數據,接收數據的方式是直接和網絡進程建立數據管道;
(6)最后,渲染進程會向瀏覽器進程“確認提交”,這是告訴瀏覽器進程:“已經準備好接受和解析頁面數據了”。
(7)瀏覽器進程接收到渲染進程“提交文檔”的消息之后,便開始移除之前舊的文檔,然后更新瀏覽器進程中的頁面狀態。
這其中,用戶發出 URL 請求到頁面開始解析的這個過程,就叫做導航。
1)用戶輸入
當用戶在地址欄中輸入一個查詢關鍵字時,地址欄會判斷輸入的關鍵字是搜索內容,還是請求的 URL。
當用戶輸入關鍵字并鍵入回車之后,這意味著當前頁面即將要被替換成新的頁面,不過在這個流程繼續之前,瀏覽器還給了當前頁面一次執行 事件的機會。
事件允許頁面在退出之前執行一些數據清理操作,還可以詢問用戶是否要離開當前頁面,比如當前頁面可能有未提交完成的表單等情況,因此用戶可以通過 事件來取消導航,讓瀏覽器不再執行任何后續工作。
2)URL 請求過程
瀏覽器進程會通過進程間通信(IPC)把 URL 請求發送至網絡進程,網絡進程接收到 URL 請求后,會在這里發起真正的 URL 請求流程(參考上面的HTTP請求流程)。
3)準備渲染進程
會為每個頁面分配一個渲染進程,也就是說,每打開一個新頁面就會配套創建一個新的渲染進程。
但如果屬于同一站點,即根域名(例如)和協議(例如 或者 )都相同,那么新頁面會復用父頁面的渲染進程。官方把這個默認策略叫 -per-site-。
4)提交文檔
提交文檔,就是指瀏覽器進程將網絡進程接收到的 HTML 數據提交給渲染進程,具體流程是這樣的:
(1)首先當瀏覽器進程接收到網絡進程的響應頭數據之后,便向渲染進程發起“提交文檔”的消息;
(2)渲染進程接收到“提交文檔”的消息后,會和網絡進程建立傳輸數據的“管道”;
(3)等文檔數據傳輸完成之后,渲染進程會返回“確認提交”的消息給瀏覽器進程;
(4)瀏覽器進程在收到“確認提交”的消息后,會更新瀏覽器界面狀態,包括了安全狀態、地址欄的 URL、前進后退的歷史狀態,并更新 Web 頁面。
5)渲染階段
一旦文檔被提交,渲染進程便開始頁面解析和子資源加載。
一旦頁面生成完成,渲染進程會發送一個消息給瀏覽器進程,瀏覽器接收到消息后,會停止標簽圖標上的加載動畫。
四、渲染流程
HTML 的內容是由標記和文本組成。標記也稱為標簽,每個標簽都有它自己的語義,瀏覽器會根據標簽的語義來正確展示 HTML 內容。
如果需要改變 HTML 的字體顏色、大小等信息,就需要用到 CSS。CSS 又稱為層疊樣式表,是由選擇器和屬性組成。
至于 (簡稱為 JS),使用它可以使網頁的內容“動”起來。
由于渲染機制過于復雜,所以渲染模塊在執行過程中會被劃分為很多子階段,輸入的 HTML 經過這些子階段,最后輸出像素瀏覽器工作原理是怎樣的,這樣的一個處理流程叫做渲染流水線。
按照渲染的時間順序,流水線可分為如下幾個子階段:構建 DOM 樹、樣式計算、布局階段、分層、繪制、分塊、光柵化和合成。
1)構建 DOM 樹
瀏覽器無法直接理解和使用 HTML,所以需要將 HTML 轉換為瀏覽器能夠理解的結構——DOM 樹,如下所示。
2)樣式計算
樣式計算( Style)地目的是為了計算出 DOM 節點中每個元素的具體樣式,這個階段大體可分為三步來完成。
(1)把 CSS 轉換為瀏覽器能夠理解的結構——。
(2)轉換樣式表中的屬性值,使其標準化,如 2em、blue、bold,需要將它們轉換為渲染引擎容易理解的、標準化的計算值。
(3)計算出 DOM 樹中每個節點的具體樣式,涉及到 CSS 的繼承規則和層疊規則。此階段最終輸出的內容是每個 DOM 節點的樣式,并被保存在 的結構內。
3)布局階段
布局就是計算 DOM 樹中可見元素幾何位置的過程。 在布局階段需要完成兩個任務:創建布局樹和布局計算。
(1)創建只包含可見元素的布局樹。
(2)計算布局樹節點的坐標位置。
4)分層
渲染引擎還需要為特定的節點生成專用的圖層,并生成一棵對應的圖層樹()。
打開 的“開發者工具”,選擇“”標簽瀏覽器工作原理是怎樣的,就可以可視化頁面的分層情況,如下圖所示。
下面再來看看這些圖層和布局樹節點之間的關系。通常情況下,并不是布局樹的每個節點都包含一個圖層,如果一個節點沒有對應的層,那么這個節點就從屬于父節點的圖層。
滿足下面兩點中任意一點的元素就可以被提升為單獨的一個圖層。
(1)擁有層疊上下文屬性的元素,例如明確定位屬性的元素、定義透明屬性的元素、使用 CSS 濾鏡的元素等。
(2)需要剪裁(clip)的地方也會被創建為圖層,如果出現滾動條,滾動條也會被提升為單獨的層。
5)圖層繪制
渲染引擎實現圖層的繪制與之類似,會把一個圖層的繪制拆分成很多小的繪制指令,然后再把這些指令按照順序組成一個待繪制列表,如下圖所示:
繪制列表中的指令其實非常簡單,比如繪制粉色矩形或者黑色的線等。而繪制一個元素通常需要好幾條繪制指令,因為每個元素的背景、前景、邊框都需要單獨的指令去繪制。
6)柵格化操作
繪制列表只是用來記錄繪制順序和繪制指令的列表,而實際上繪制操作是由渲染引擎中的合成線程來完成的。
當圖層的繪制列表準備好之后,主線程會把該繪制列表提交()給合成線程。
合成線程會將圖層劃分為圖塊(tile),這些圖塊的大小通常是 或者 ,如下圖所示:
然后合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作是由柵格化來執行的。
所謂柵格化(),是指將圖塊轉換為位圖。而圖塊是柵格化執行的最小單位。渲染進程維護了一個柵格化的線程池,所有的圖塊柵格化都是在線程池內執行的。
通常,柵格化過程都會使用 GPU 來加速生成,使用 GPU 生成位圖的過程叫快速柵格化,或者 GPU 柵格化,生成的位圖被保存在 GPU 內存中。
從圖中可以看出,渲染進程把生成圖塊的指令發送給 GPU,然后在 GPU 中執行生成圖塊的位圖,并保存在 GPU 的內存中。
7)合成和顯示
一旦所有圖塊都被光柵化,合成線程就會生成一個繪制圖塊的命令——“”,然后將該命令提交給瀏覽器進程。
將其頁面內容繪制到內存中,最后再將內存顯示在屏幕上。
一個完整的渲染流程大致可總結為如下:
(1)渲染進程將 HTML 內容轉換為能夠讀懂的 DOM 樹結構。
(2)渲染引擎將 CSS 樣式表轉化為瀏覽器可以理解的 ,計算出 DOM 節點的樣式。
(3)創建布局樹,并計算元素的布局信息。
(4)對布局樹進行分層,并生成分層樹。
(5)為每個圖層生成繪制列表,并將其提交到合成線程。
(6)合成線程將圖層分成圖塊,并在光柵化線程池中將圖塊轉換成位圖。
(7)合成線程發送繪制圖塊命令 給瀏覽器進程。
(8)瀏覽器進程根據 消息生成頁面,并顯示到顯示器上。
渲染引擎會通過合成線程直接去處理變換,這些變換并沒有涉及到主線程,這樣就大大提升了渲染的效率。這也是 CSS 動畫比 動畫高效的原因。
8)重排
更新了元素的幾何屬性。
從上圖可以看出,如果你通過 或者 CSS 修改元素的幾何位置屬性,例如改變元素的寬度、高度等,那么瀏覽器會觸發重新布局,解析之后的一系列子階段,這個過程就叫重排。
無疑,重排需要更新完整的渲染流水線,所以開銷也是最大的。
9)重繪
更新元素的繪制屬性。
從圖中可以看出,如果修改了元素的背景顏色,那么布局階段將不會被執行,因為并沒有引起幾何位置的變換,所以就直接進入了繪制階段,然后執行之后的一系列子階段,這個過程就叫重繪。
相較于重排操作,重繪省去了布局和分層階段,所以執行效率會比重排操作要高一些。
10)直接合成
染引擎將跳過布局和繪制,只執行后續的合成操作,把這個過程叫做合成。
在上圖中,使用了 CSS 的 來實現動畫效果,這可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。
這樣的效率是最高的,因為是在非主線程上合成,并沒有占用主線程的資源,另外也避開了布局和繪制兩個子階段,所以相對于重繪和重排,合成能大大提升繪制效率。