《瀏覽器工作原理與實踐》是極客時間上的一個瀏覽器學習系列,在學習之后特在此做記錄和總結。
一、Chrome架構
1)線程和進程
Chrome打開一個頁面會啟動4個進程:網絡進程、GPU進程、瀏覽器主進程和渲染進程。
一個進程就是一個程序的運行實例,啟動一個程序的時候,操作系統會為該程序創建一塊內存,用來存放代碼、運行中的數據和一個執行任務的主線程,這樣一個運行環境叫進程。
線程是依附於進程的,而進程中使用多線程並行處理能提升運算效率。
進程和線程之間的關系有以下 4 個特點:
(1)進程中的任意一線程執行出錯,都會導致整個進程的崩潰。
(2)線程之間共享進程中的數據。
(3)當一個進程關閉之后,操作系統會回收進程所占用的內存。
(4)進程之間的內容相互隔離。
2)Chrome進程架構
最新的 Chrome 瀏覽器包括:1 個瀏覽器(Browser)主進程、1 個 GPU 進程、1 個網絡(NetWork)進程、多個渲染進程和多個插件進程,如下圖所示。
(1)瀏覽器主進程:負責界面顯示、用戶交互、子進程管理,同時提供存儲等功能。
(2)渲染進程:核心任務是將 HTML、CSS 和 JavaScript 轉換為用戶可以與之交互的網頁,排版引擎 Blink 和 JavaScript 引擎 V8 都是運行在該進程中,默認情況下,Chrome 會為每個 Tab 標簽創建一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下。
(3)GPU進程:GPU 的使用初衷是為了實現 3D CSS 的效果,隨后網頁、Chrome 的 UI 界面都選擇采用 GPU 來繪制,這使得 GPU 成為瀏覽器普遍的需求。
(4)網絡進程:負責頁面的網絡資源加載,之前是作為一個模塊運行在瀏覽器進程里面的。
(5)插件進程:負責插件的運行,因插件易崩潰,所以需要通過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面造成影響。
二、網絡協議
1)TCP協議
在衡量 Web 頁面性能時有一個重要的指標FP(First Paint),它是指從頁面加載到首次開始繪制的時長。而影響 FP 指標的一個重要因素就是網絡加載速度。
互聯網,實際上是一套理念和協議組成的體系架構。互聯網中的數據是通過數據包來傳輸的。
(1)IP:把數據包送達目的主機。
(2)UDP:把數據包送達應用程序。
(3)TCP:把數據完整地送達應用程序。
2)HTTP協議
HTTP 是一種允許瀏覽器向服務器獲取資源的協議,是 Web 的基礎。
瀏覽器端發起 HTTP 請求流程:
(1)構建請求,構建好后,瀏覽器准備發起網絡請求。
(2)查找緩存,當瀏覽器發現請求的資源已經在瀏覽器緩存中存有副本,它會攔截請求,返回該資源的副本,並直接結束請求。
(3)准備 IP 地址和端口,HTTP 的內容是通過 TCP 來傳輸的,第一步就是先建立TCP連接,而請求 DNS(DNS數據緩存服務) 可返回域名對應的 IP。
(4)等待 TCP 隊列,同一個域名同時最多只能建立 6 個 TCP 連接,如果同時有 10 個請求發生,那么其中 4 個會進入等待狀態。
(5)建立 TCP 連接,快樂地和服務器握手。
(6)發送 HTTP 請求,和服務器進行通信,向服務器發送請求行,包括請求方法、請求 URI(Uniform Resource Identifier)和 HTTP 版本協議。
服務器端處理 HTTP 請求流程:
(1)返回請求,一旦服務器處理結束,便可以返回響應行,包括協議版本和狀態碼。
(2)斷開連接,但瀏覽器或者服務器在其頭信息中加入了:Connection:Keep-Alive,那么 TCP 連接在發送后將仍然保持打開狀態。
(3)重定向,當兩個 URL 不一樣時,會涉及一個重定向操作。
為什么很多站點第二次打開速度會很快?因為 DNS 緩存和頁面資源緩存這兩塊數據是會被瀏覽器緩存的。
登錄狀態是如何保持的?瀏覽器頁面狀態是通過使用 Cookie 來實現的。如果服務器端發送的響應頭內有 Set-Cookie 的字段,那么瀏覽器就會將該字段的內容保存到本地。
下面是張詳細的“HTTP 請求示意圖”,用來展現瀏覽器中的 HTTP 請求所經歷的各個階段。
瀏覽器中的 HTTP 請求從發起到結束一共經歷了八個階段:構建請求、查找緩存、准備 IP 和端口、等待 TCP 隊列、建立 TCP 連接、發起 HTTP 請求、服務器處理請求、服務器返回請求和斷開連接。
三、導航流程
在瀏覽器里,從輸入 URL 到頁面展示,這中間發生了什么?其中涉及到了網絡、操作系統、Web 等一系列的知識,如下圖所示。
整個流程大致描述如下:
(1)首先,瀏覽器進程接收到用戶輸入的 URL 請求,瀏覽器進程便將該 URL 轉發給網絡進程。
(2)然后,在網絡進程中發起真正的 URL 請求。
(3)接着,網絡進程接收到了響應頭數據,便解析響應頭數據,並將數據轉發給瀏覽器進程。
(4)瀏覽器進程接收到網絡進程的響應頭數據之后,發送“提交導航 (Commit Navigation)”消息到渲染進程;
(5)渲染進程接收到“提交導航”的消息之后,便開始准備接收 HTML 數據,接收數據的方式是直接和網絡進程建立數據管道;
(6)最后,渲染進程會向瀏覽器進程“確認提交”,這是告訴瀏覽器進程:“已經准備好接受和解析頁面數據了”。
(7)瀏覽器進程接收到渲染進程“提交文檔”的消息之后,便開始移除之前舊的文檔,然后更新瀏覽器進程中的頁面狀態。
這其中,用戶發出 URL 請求到頁面開始解析的這個過程,就叫做導航。
1)用戶輸入
當用戶在地址欄中輸入一個查詢關鍵字時,地址欄會判斷輸入的關鍵字是搜索內容,還是請求的 URL。
當用戶輸入關鍵字並鍵入回車之后,這意味着當前頁面即將要被替換成新的頁面,不過在這個流程繼續之前,瀏覽器還給了當前頁面一次執行 beforeunload 事件的機會。
beforeunload 事件允許頁面在退出之前執行一些數據清理操作,還可以詢問用戶是否要離開當前頁面,比如當前頁面可能有未提交完成的表單等情況,因此用戶可以通過 beforeunload 事件來取消導航,讓瀏覽器不再執行任何后續工作。
2)URL 請求過程
瀏覽器進程會通過進程間通信(IPC)把 URL 請求發送至網絡進程,網絡進程接收到 URL 請求后,會在這里發起真正的 URL 請求流程(參考上面的HTTP請求流程)。
3)准備渲染進程
Chrome 會為每個頁面分配一個渲染進程,也就是說,每打開一個新頁面就會配套創建一個新的渲染進程。
但如果屬於同一站點,即根域名(例如geekbang.org)和協議(例如https:// 或者 http://)都相同,那么新頁面會復用父頁面的渲染進程。官方把這個默認策略叫 process-per-site-instance。
4)提交文檔
提交文檔,就是指瀏覽器進程將網絡進程接收到的 HTML 數據提交給渲染進程,具體流程是這樣的:
(1)首先當瀏覽器進程接收到網絡進程的響應頭數據之后,便向渲染進程發起“提交文檔”的消息;
(2)渲染進程接收到“提交文檔”的消息后,會和網絡進程建立傳輸數據的“管道”;
(3)等文檔數據傳輸完成之后,渲染進程會返回“確認提交”的消息給瀏覽器進程;
(4)瀏覽器進程在收到“確認提交”的消息后,會更新瀏覽器界面狀態,包括了安全狀態、地址欄的 URL、前進后退的歷史狀態,並更新 Web 頁面。
5)渲染階段
一旦文檔被提交,渲染進程便開始頁面解析和子資源加載。
一旦頁面生成完成,渲染進程會發送一個消息給瀏覽器進程,瀏覽器接收到消息后,會停止標簽圖標上的加載動畫。
四、渲染流程
HTML 的內容是由標記和文本組成。標記也稱為標簽,每個標簽都有它自己的語義,瀏覽器會根據標簽的語義來正確展示 HTML 內容。
如果需要改變 HTML 的字體顏色、大小等信息,就需要用到 CSS。CSS 又稱為層疊樣式表,是由選擇器和屬性組成。
至於 JavaScript(簡稱為 JS),使用它可以使網頁的內容“動”起來。
由於渲染機制過於復雜,所以渲染模塊在執行過程中會被划分為很多子階段,輸入的 HTML 經過這些子階段,最后輸出像素,這樣的一個處理流程叫做渲染流水線。
按照渲染的時間順序,流水線可分為如下幾個子階段:構建 DOM 樹、樣式計算、布局階段、分層、繪制、分塊、光柵化和合成。
1)構建 DOM 樹
瀏覽器無法直接理解和使用 HTML,所以需要將 HTML 轉換為瀏覽器能夠理解的結構——DOM 樹,如下所示。
2)樣式計算
樣式計算(Recalculate Style)地目的是為了計算出 DOM 節點中每個元素的具體樣式,這個階段大體可分為三步來完成。
(1)把 CSS 轉換為瀏覽器能夠理解的結構——styleSheets。
(2)轉換樣式表中的屬性值,使其標准化,如 2em、blue、bold,需要將它們轉換為渲染引擎容易理解的、標准化的計算值。
(3)計算出 DOM 樹中每個節點的具體樣式,涉及到 CSS 的繼承規則和層疊規則。此階段最終輸出的內容是每個 DOM 節點的樣式,並被保存在 ComputedStyle 的結構內。
3)布局階段
布局就是計算 DOM 樹中可見元素幾何位置的過程。Chrome 在布局階段需要完成兩個任務:創建布局樹和布局計算。
(1)創建只包含可見元素的布局樹。
(2)計算布局樹節點的坐標位置。
4)分層
渲染引擎還需要為特定的節點生成專用的圖層,並生成一棵對應的圖層樹(LayerTree)。
打開 Chrome 的“開發者工具”,選擇“Layers”標簽,就可以可視化頁面的分層情況,如下圖所示。
下面再來看看這些圖層和布局樹節點之間的關系。通常情況下,並不是布局樹的每個節點都包含一個圖層,如果一個節點沒有對應的層,那么這個節點就從屬於父節點的圖層。
滿足下面兩點中任意一點的元素就可以被提升為單獨的一個圖層。
(1)擁有層疊上下文屬性的元素,例如明確定位屬性的元素、定義透明屬性的元素、使用 CSS 濾鏡的元素等。
(2)需要剪裁(clip)的地方也會被創建為圖層,如果出現滾動條,滾動條也會被提升為單獨的層。
5)圖層繪制
渲染引擎實現圖層的繪制與之類似,會把一個圖層的繪制拆分成很多小的繪制指令,然后再把這些指令按照順序組成一個待繪制列表,如下圖所示:
繪制列表中的指令其實非常簡單,比如繪制粉色矩形或者黑色的線等。而繪制一個元素通常需要好幾條繪制指令,因為每個元素的背景、前景、邊框都需要單獨的指令去繪制。
6)柵格化操作
繪制列表只是用來記錄繪制順序和繪制指令的列表,而實際上繪制操作是由渲染引擎中的合成線程來完成的。
當圖層的繪制列表准備好之后,主線程會把該繪制列表提交(commit)給合成線程。
合成線程會將圖層划分為圖塊(tile),這些圖塊的大小通常是 256x256 或者 512x512,如下圖所示:
然后合成線程會按照視口附近的圖塊來優先生成位圖,實際生成位圖的操作是由柵格化來執行的。
所謂柵格化(raster),是指將圖塊轉換為位圖。而圖塊是柵格化執行的最小單位。渲染進程維護了一個柵格化的線程池,所有的圖塊柵格化都是在線程池內執行的。
通常,柵格化過程都會使用 GPU 來加速生成,使用 GPU 生成位圖的過程叫快速柵格化,或者 GPU 柵格化,生成的位圖被保存在 GPU 內存中。
從圖中可以看出,渲染進程把生成圖塊的指令發送給 GPU,然后在 GPU 中執行生成圖塊的位圖,並保存在 GPU 的內存中。
7)合成和顯示
一旦所有圖塊都被光柵化,合成線程就會生成一個繪制圖塊的命令——“DrawQuad”,然后將該命令提交給瀏覽器進程。
將其頁面內容繪制到內存中,最后再將內存顯示在屏幕上。
一個完整的渲染流程大致可總結為如下:
(1)渲染進程將 HTML 內容轉換為能夠讀懂的 DOM 樹結構。
(2)渲染引擎將 CSS 樣式表轉化為瀏覽器可以理解的 styleSheets,計算出 DOM 節點的樣式。
(3)創建布局樹,並計算元素的布局信息。
(4)對布局樹進行分層,並生成分層樹。
(5)為每個圖層生成繪制列表,並將其提交到合成線程。
(6)合成線程將圖層分成圖塊,並在光柵化線程池中將圖塊轉換成位圖。
(7)合成線程發送繪制圖塊命令 DrawQuad 給瀏覽器進程。
(8)瀏覽器進程根據 DrawQuad 消息生成頁面,並顯示到顯示器上。
渲染引擎會通過合成線程直接去處理變換,這些變換並沒有涉及到主線程,這樣就大大提升了渲染的效率。這也是 CSS 動畫比 JavaScript 動畫高效的原因。
8)重排
更新了元素的幾何屬性。
從上圖可以看出,如果你通過 JavaScript 或者 CSS 修改元素的幾何位置屬性,例如改變元素的寬度、高度等,那么瀏覽器會觸發重新布局,解析之后的一系列子階段,這個過程就叫重排。
無疑,重排需要更新完整的渲染流水線,所以開銷也是最大的。
9)重繪
更新元素的繪制屬性。
從圖中可以看出,如果修改了元素的背景顏色,那么布局階段將不會被執行,因為並沒有引起幾何位置的變換,所以就直接進入了繪制階段,然后執行之后的一系列子階段,這個過程就叫重繪。
相較於重排操作,重繪省去了布局和分層階段,所以執行效率會比重排操作要高一些。
10)直接合成
染引擎將跳過布局和繪制,只執行后續的合成操作,把這個過程叫做合成。
在上圖中,使用了 CSS 的 transform 來實現動畫效果,這可以避開重排和重繪階段,直接在非主線程上執行合成動畫操作。
這樣的效率是最高的,因為是在非主線程上合成,並沒有占用主線程的資源,另外也避開了布局和繪制兩個子階段,所以相對於重繪和重排,合成能大大提升繪制效率。