一文摸透從輸入URL到頁面渲染的過程
阿新 • • 發佈:2020-04-07
# 一文摸透從輸入`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