1. 程式人生 > >一文摸透從輸入URL到頁面渲染的過程

一文摸透從輸入URL到頁面渲染的過程

# 一文摸透從輸入`URL`到頁面渲染的過程 從輸入`URL`到頁面渲染需要`Chrome`瀏覽器的多個程序配合,所以我們先來談談現階段`Chrome`瀏覽器的多程序架構。 ## 一、`Chrome`架構 目前`Chrome`採用的是多程序的架構模式,可分為主要的五類程序,分別是:瀏覽器(`Browser`)主程序、 `GPU` 程序、網路(`NetWork`)程序、多個渲染程序和多個外掛程序; ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/0.1.png) - **瀏覽器程序**。主要負責介面顯示、使用者互動、子程序管理,同時提供儲存等功能。 - **渲染程序**。核心任務是將` HTML`、`CSS` 和 `JavaScript `轉換為使用者可以與之互動的網頁,排版引擎`Blink`和`JavaScript`引擎`V8`都是執行在該程序中,預設情況下,`Chrome`會為每個`Tab`標籤建立一個渲染程序。出於安全考慮,渲染程序都是執行在沙箱模式下。 - **`GPU`程序**。其實,`Chrome`剛開始釋出的時候是沒有`GPU`程序的。而`GPU`的使用初衷是為了實現`3D CSS`的效果,只是隨後網頁、`Chrome`的`UI`介面都選擇採用`GPU`來繪製,這使得`GPU`成為瀏覽器普遍的需求。最後,`Chrome`在其多程序架構上也引入了`GPU`程序。 - **網路程序**。主要負責頁面的網路資源載入,之前是作為一個模組執行在瀏覽器程序裡面的,直至最近才獨立出來,成為一個單獨的程序。 - **外掛程序**。主要是負責外掛的執行,因外掛易崩潰,所以需要通過外掛程序來隔離,以保證外掛程序崩潰不會對瀏覽器和頁面造成影響 瞭解了`Chrome`的多程序架構,就能夠從巨集觀上理解從輸入`URL`到頁面渲染的過程了,這個過程主要分為**導航階段**和**渲染階段**。 ## 二、導航階段 ### Ⅰ.瀏覽器主程序 #### 1.使用者輸入`URL` * **1、**瀏覽器程序檢查`url`,組裝協議,構成完整的`url`,這時候有兩種情況: * 輸入的是搜尋內容:位址列會使用瀏覽器預設的搜尋引擎,來合成新的帶搜尋關鍵字的`URL`。 * 輸入的是請求`URL`:位址列會根據規則,給這段內容加上協議,合成為完整的`URL`; * **2、**瀏覽器程序通過程序間通訊(`IPC`)把`url`請求傳送給網路程序; ### Ⅱ.網路程序 #### 2.`URL`請求過程 * **3、**網路程序接收到`url`請求後檢查本地快取是否快取了該請求資源,如果有則將該資源返回給瀏覽器程序; > 這裡涉及到瀏覽器的快取策略問題,有興趣的可以上網查閱相關資料。 * **4、**準備`IP`地址和埠:進行`DNS`解析時先查詢快取,沒有再使用`DNS`伺服器解析,查詢順序為: * 瀏覽器快取; * 本機快取; * `hosts`檔案; * 路由器快取; * `ISP DNS`快取; * `DNS`遞迴查詢(本地`DNS`伺服器 -> 許可權`DNS`伺服器 -> 頂級`DNS`伺服器 -> `13`臺根`DNS`伺服器) * **5、**等待`TCP`佇列:瀏覽器會為每個域名最多維護`6`個`TCP`連線,如果發起一個`HTTP`請求時,這 `6`個 `TCP`連線都處於忙碌狀態,那麼這個請求就會處於排隊狀態;解決方案: * 採用域名分片技術:將一個站點的資源放在多個(`CDN`)域名下面。 * 升級為`HTTP2`,就沒有`6`個`TCP`連線的限制了; * **6、**通過三次握手建立`TCP`連線: ![123](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/0.2.png) * **第一次:**客戶端先向伺服器端傳送一個同步資料包,報文的`TCP`首部中:標誌位:**同步`SYN`**為`1`,表示這是一個請求建立連線的資料包;序號`Seq=x`,`x`為所傳送資料的第一個位元組的序號,隨後進入`SYN-SENT`狀態; > 標誌位值為`1`表示該標誌位有效。 * **第二次:**伺服器根據收到資料包的`SYN`標誌位判斷為建立連線的請求,隨後返回一個確認資料包,其中標誌位`SYN=1`,`ACK=1`,序號`seq=y`,確認號`ack=x + 1`表示收到了客戶端傳輸過來的`x`位元組資料,並希望下次從`x+1`個位元組開始傳,並進入`SYN-RCVD`狀態; > 這裡要區分標誌位`ACK`和確認號`ack`; * **第三次:**客戶端收到後,再給伺服器傳送一個確認資料包,標誌位`ACK=1`,序號`seq=x+1`,確認號`ack=y+1`,隨後進入`ESTABLISHED`狀態; 伺服器端收到後,也進入`ESTABLISHED`狀態,由此成功建立了`TCP`連線,可以開始資料傳送; * **為什麼要第三次揮手?**避免伺服器等待造成**資源浪費**,具體原因: > 如果沒有最後一個數據包確認(第三次握手),`A`先發出一個建立連線的請求資料包,由於網路原因繞遠路了。`A`經過設定的超時時間後還未收到`B`的確認資料包。 > > 於是發出第二個建立連線的請求資料包,這次網路通暢,`B`的確認資料包也很快就到達`A`。於是`A`與`B`開始傳輸資料; > > 過了一會`A`第一次發出的建立連線的請求資料包到達了`B`,`B`以為是再次建立連線,所以又發出一個確認資料包。由於A已經收到了一個確認資料包,所以會忽略`B`發來的第二個確認資料包,但是`B`發出確認資料包之後就要一直等待`A`的回覆,而`A`永遠也不會回覆。 > > 由此造成伺服器資源浪費,這種情況多了`B`計算機可能就停止響應了。 * **7、**構建併發送`HTTP`請求資訊; * **8、**伺服器端處理請求; * **9、**客戶端處理響應,首先檢查伺服器響應報文的狀態碼: * 如果是`301/302`表示伺服器已更換域名需要重定向,這時網路程序會從響應頭的`Location`欄位裡面讀取重定向的地址,然後再發起新的`HTTP`或者`HTTPS`請求,跳回第`4`步。 * 如果是`200`,就檢查`Content-Type`欄位,值為`text/html`說明是`HTML`文件,是`application/octet-stream`說明是檔案下載; ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/1.png) * **10、**請求結束,當通用首部欄位`Conection`不是`Keep-Alive`時,即不為`TCP`長連線時,通過四次揮手斷開`TCP`連線: ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/1.5.png) * **第一次:**客戶端(主動斷開連線)傳送資料包給伺服器,其中標誌位`FIN=1`,序號位`seq=u`,並停止傳送資料; * **第二次:**伺服器收到資料包後,由於還需傳輸資料,無法立即關閉連線,先返回一個標誌位`ACK=1`,序號`seq=v`,確認號`ack=u+1`的資料包; * **第三次:**伺服器準備好斷開連線後,返回一個數據包,其中標誌位`FIN=1`,標誌位`ACK=1`,序號`seq=w`,確認號`ack=u+1`; * **第四次:**客戶端收到資料包後,返回一個標誌位`ACK=1`,序號`seq=u+1`,確認號`ack=w+1`的資料包。 由此通過四次揮手斷開`TCP`連線。 > 詳細過程參見:[詳解TCP連線的“三次握手”與“四次揮手”(上)](https://www.cnblogs.com/AhuntSun-blog/p/12028636.html) * **為什麼要四次揮手?**由於伺服器不能馬上斷開連線,導致`FIN`釋放連線報文與`ACK`確認接收報文需要分兩次傳輸,即第二次和第三次"揮手"; #### 3.準備渲染程序 * **11、**準備渲染程序:瀏覽器程序檢查當前`url`是否與之前打開了渲染程序的頁面的根域名相同,如果相同,則複用原來的程序,如果不同,則開啟新的渲染程序; #### 4.提交文件 * **12、**提交文件: * **渲染程序**準備好後,**瀏覽器**向**渲染程序**發起“**提交文件**”的訊息,**渲染程序**接收到訊息後與**網路程序**建立傳輸資料的“**管道**” * **渲染程序**接收完資料後,向瀏覽器傳送“**確認提交**” * **瀏覽器程序**接收到確認訊息後更新瀏覽器介面狀態:**安全狀態**、**位址列`url`**、**前進後退的歷史狀態**、**更新`web`頁面** ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/2.png) ## 三、渲染階段 在渲染階段通過**渲染流水線**在渲染程序的主執行緒和合成執行緒配合下,完成頁面的渲染; ### Ⅲ.渲染程序 ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/3.png) > **渲染程序中的主執行緒部分** #### 5.構建`DOM`樹 * **13、**先將請求回來的資料解壓,隨後`HTML`解析器將其中的`HTML`**位元組流**通過**分詞器**拆分為一個個`Token`,然後生成節點`Node`,最後解析成瀏覽器識別的`DOM`樹結構。 可以通過`Chrome`除錯工具的`Console`選項開啟控制檯輸入`document`檢視`DOM`樹; > 渲染引擎還有一個**安全檢查模組**叫 `XSSAuditor`,是用來**檢測詞法安全**的。在分詞器解析出來 `Token` 之後,它會檢測這些模組是否安全,比如**是否引用了外部指令碼**,**是否符合 `CSP` 規範**,**是否存在跨站點請求**等。如果出現不符合規範的內容,`XSSAuditor` 會對該指令碼或者下載任務**進行攔截**。 首次解析`HTML`時**渲染程序**會開啟一個**預解析執行緒**,遇到`HTML`文件中內嵌的`JavaScript`和`CSS`外部引用就會同步提前下載這些檔案,下載時間以最後下載完的檔案為準。 ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/4.png) #### 6.構建`CSSOM` * **14、**`CSS`解析器將`CSS`轉換為瀏覽器能識別的`styleSheets`也就是`CSSOM`:可以通過控制檯輸入`document.styleSheets`檢視; 這裡要考慮一下阻塞的問題,由於`JavaScript`有修改`CSS`和`HTML`的能力,所以,需要先等到 `CSS` 檔案下載完成並生成 `CSSOM`,然後再執行 `JavaScript` 指令碼,最後再繼續構建 `DOM`。由於這種阻塞,導致了**解析白屏**; > **優化方案:** > > * **移除`js`和`css`的檔案下載**:通過內聯 `JavaScript`、內聯 `CSS`; > * **儘量減少檔案大小**:如通過 `webpack` 等工具**移除**不必要的**註釋**,並**壓縮 `js` 檔案**; > * 將不進行`DOM`操作或`CSS`樣式修改的 `JavaScript` 標記上 `sync` 或者 `defer`非同步引入; > * **使用媒體查詢屬性**:將大的`CSS`檔案拆分成多個不同用途的 `CSS` 檔案,只有在特定的場景下才會載入特定的 `CSS` 檔案。 可以通過瀏覽器除錯工具的`Network`面板中的`DOMContentLoaded`檢視最後生成`DOM`樹所需的時間; ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/5.png) ![image-20200405110720560](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/6.png) #### 7.樣式計算 * **15、**轉換樣式表中的屬性值,使其標準化。比如將`em`轉換為`px`,`color`轉換為`rgb`; * **16、**計算`DOM`樹中每個節點的具體樣式,這裡遵循`CSS`的繼承和層疊規則;可以通過`Chrome`除錯工具的`Elements`選項的`Computed`檢視某一標籤的最終樣式; ![image-20200405110849074](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/7.png) #### 8.佈局階段 * **17、**建立佈局樹,遍歷`DOM`樹中的所有節點,去掉所有隱藏的節點(比如`head`,添加了`display:none`的節點),只在佈局樹中保留可見的節點。 * **18、**計算佈局樹中節點的座標位置(較複雜,這裡不展開); #### 9.分層 * **19、**對佈局樹進行分層,並生成分層樹(`Layer Tree`),可以通過`Chrome`除錯工具的`Layer`選項檢視。分層樹中每一個節點都直接或間接的屬於一個圖層(如果一個節點沒有對應的層,那麼這個節點就從屬於父節點的圖層) ![image-20200405111350260](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/8.png) #### 10.圖層繪製 * **20、**為每個圖層生成繪製列表(即繪製指令),並將其提交到合成執行緒。以上操作都是在渲染程序中的主執行緒中進行的,提交到合成執行緒後就不阻塞主執行緒了; ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/9.png) > **渲染程序中的合成執行緒部分** ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/10.png) #### 11.切分圖塊 **21、**合成執行緒將圖層切分成大小固定的圖塊(`256x256`或者`512x512`)然後**優先繪製**靠近視口的圖塊,這樣就可以大大加速頁面的顯示速度; ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/11.png) ### Ⅳ.`GPU`程序 #### 12.柵格化操作 * **22、**在**光柵化執行緒池**中將**圖塊**轉換成**點陣圖**,通常這個過程都會使用`GPU`來加速生成,使用`GPU`生成點陣圖的過程叫**快速柵格化**,或者`GPU`柵格化,生成的點陣圖被儲存在`GPU`記憶體中。 ![](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/12.png) ### Ⅴ.瀏覽器主程序 #### 13.合成與顯示 * **23、**合成:一旦所有圖塊都被光柵化,**合成執行緒**就會將它們合成為一張圖片,並生成一個繪製圖塊的命令——“`DrawQuad`”,然後將該命令提交給瀏覽器程序。 > **注意了:**合成的過程是在渲染程序的**合成執行緒**中完成的,不會影響到渲染程序的**主執行緒**執行; * **24、**顯示:瀏覽器程序裡面有一個叫`viz`的元件,用來接收合成執行緒發過來的`DrawQuad`命令,然後根據`DrawQuad`命令,將其頁面內容繪製到記憶體中,最後再將記憶體顯示在螢幕上。 到這裡,經過這一系列的階段,編寫好的`HTML`、`CSS`、`JavaScript`等檔案,經過瀏覽器就會顯示出漂亮的頁面了。 > 參考資料:[瀏覽器工作原理與實踐](https://time.geekbang.org/column/intro/10