全方位解析瀏覽器渲染原理

語言: CN / TW / HK

寫在前邊

很久之前就想關於瀏覽器渲染原理做一份總結性文章。之前也看過網上不少這個方面的文章,關於瀏覽器渲染機制的原理文章非常多但是總是覺得差那麼一點意思,沒有串聯整個流程。所以這裡打算串聯這兩部分,從原理和實踐出發講解他們背後的含義。

文章中可能包含許多原理部分知識,如果大家對於這部分有疑惑的話我們可以在評論區互相交流。

程序和執行緒

  • 程序是作業系統資源分配的最小單位,程序中包含執行緒。
  • 執行緒是受程序管理的,瀏覽器中採用的是多程序模式。

日常中我們使用瀏覽器是基於一個一個tab頁進行訪問網站,如果說某一個tab頁面掛掉了其實對於其他tab頁是沒有任何影響的,其實每一個tab頁就是一個單獨的程序。

他們之間互相獨立互不影響。

我們來看看這張圖:

image.png

瀏覽器中的程序分為下列5個:

  1. 瀏覽器程序: 你可以理解瀏覽器程序為一個統一的"排程大師"去排程其他程序,比如我們在位址列輸入url時,瀏覽器程序首先會呼叫網路程序。 它可以做一些子程序管理以及一些儲存的處理。

  2. 渲染程序: 這個程序對於我們來說是最重要的一個程序,每一個tab頁都擁有獨立的渲染程序,它的主要作用是渲染頁面。

  3. 網路程序: 這個程序是控制對於一些靜態資源的請求,它將資源請求完成之後會交給渲染程序進行渲染。

  4. GPU程序: 這個程序可以呼叫硬體進行渲染,從而實現渲染加速。比如translate3dcss3屬性會騙取呼叫GPU程序從而開啟硬體加速。

  5. 外掛程序: chrome中的外掛也是一個獨立的程序。

各個程序之間是互相獨立不影響的,這裡讓我們先對瀏覽器這5個程序有一個大概的認識,清楚每一個各自的大概作用。

從輸入URL到頁面顯示之間究竟發生了什麼

我相信每一個合格的前端工程師對這個問題都已經瞭然於胸,網路上也有很多關於它的答案。

但是這裡大多數人對於這個題理解的過於膚淺了,我會盡量從各個方面為大家來解釋這個過程。

網路資源層面

首先我們先拋開瀏覽器對於資源的處理過程,先來看看一次正常的url輸入在資源載入方面經歷的生命週期。

當我們在位址列中輸入了一個url時,瀏覽器程序會監聽到這次互動。緊接著它會分配出一個渲染程序進行準備渲染頁面,同時瀏覽器程序會呼叫網路程序載入資源。

等待網路程序載入完成資源後會將資源交給渲染程序進行頁面的渲染。從程序角度來說整體的載入流程就是這樣。

大的方面來說就是瀏覽器程序進行排程,載入程序載入完成資源後交給渲染程序進行渲染載入的資源。

接下來我們詳細看看輸入url之後的請求過程中究竟發生了哪些事情。

網路七層協議(OSI)

我們來稍微看一下這個圖:

image.png

對於這塊不瞭解的同學可以稍微看一下一次網路請求涉及到的七個階段。

有關他們的每個部分的詳細介紹你可以在這裡檢視

我可以將這七層歸為下列四層:

  • 通常我們會將應用層、表示層、會話層統稱為應用層,應用層的主要協議就是HTTP協議。

  • 傳輸層中我們瀏覽器中Http協議是基於tcp去進行網路傳輸。(常見傳輸協議的有tcp還有udp)

  • 網路層中一般都是ip協議。

  • 當然在資料鏈路層和物理層都是被稱為物理層。

讓我們先從7層協議來分析一下瀏覽器對於url載入的過程。

首先當我們輸入url輸入一個域名瀏覽器會在磁碟/記憶體快取中去查詢請求的檔案,檢視是否命中快取。如果命中快取則直接會直接從快取中拿取對應的ip地址。

如果命中強快取則會直接返回對應資源不會進入下面的步驟。

這裡我們先忽略快取帶來的影響,這裡涉及一個協商快取和強快取的知識點會在下面的內容中進行詳細講解。

假設我們是首次訪問這個頁面,此時並沒有任何快取:

如果我們訪問的這個域名沒有被解析過,那麼我們需要解析位址列中輸入的域名。解析域名主要依靠的是DNS協議,將域名解析成為ip地址。ip地址才是真正找到對應的ip

dns你可以理解它為一個對映表,將域名和ip地址進行了對映。其實就是一個分散式的資料庫,通過域名查詢對應的ip地址。

需要注意的是dns解析是基於udp協議的而非tcp

這裡有一個小問題需要提一下,為什麼dns解析是基於udp而非tcp協議

我們的dns解析過程是一個伺服器的查詢過程。因為域名分為一級/二級...域名,所以每一級域名都會迭代去查詢如果它採用tcp協議的話,每經過一次域名查詢,域名伺服器都會經過三次握手。 如果是基於tcp協議進行域名查詢的話每一次tcp協議都會進行三次握手。但是udp就不會,他會直接發包然後確認。

相較於udptcp是更加安全,可靠的(因為三次握手以及四次揮手)但是這也造成了它相對於udp消耗更多時間。

udp常用的場景是視訊或者直播中,對於我們來說dns解析中使用udp更多的原因是因為udp的速度,當然即使丟包了,我們重新發送就可以了。

tcp傳輸的過程稱為分段傳輸,也就是會拆分為多個包,一個包一個包的進行傳送得到響應之後在傳送下一個包。這樣的方式無疑帶來的有點是更加可靠和安全。但是在時效上並不如udp協議的實時(直接通訊無需建立連線)。

此時會根據DNS解析通過域名+埠號解析出對應的IP地址。

我們擁有了ip地址之後,接下來我們就需要將利用ip進行尋找網頁地址。

此時如果我們的請求地址是https,在通過ip定址之前會額外增加一步ssl協商保證資料的安全性。

當通過ip定址成功後,瀏覽器知道了伺服器的地址。此時並不會立即將資料傳送過去,而是會進入一個排隊等待的過程。比如一個域名下有多個請求,同一個域名在http1.1下最多隻能建立6tcp連結,也就是說同一時間最多傳送6個請求,他們首先會進入一個排隊的等待時間。

排隊結束後,開始傳送請求。此時就要通過tcp先進行建立連結通過三次握手,建立完成連結之後開始傳輸資料。

上邊我們說過tcp是基於分段傳輸的,基於內容特別大的傳輸內容tcp會將資料包進行拆分稱為多個數據包進行有序傳輸

tcp傳輸過程中如果傳輸中出現了丟包,那麼tcp會進行重發。

有興趣的小夥伴可以思考下為什麼tcp連結有時是三次又是又是四次。注意disici1sh

伺服器再收到之後會按照順序進行接收。

tcp建立完成連結之後,瀏覽器會通過http請求傳送請求的資料。

一次http請求包含

  • 請求行
  • 請求頭
  • 請求體

http1.1中預設開啟了了Connection:keep-alive,它的作用是在下次傳送請求時在一定時間內可以複用上一次的tcp連結而不需要重新建立這個連結。(也就是在一定時間內保持相同域名tcp連結不斷開)。

此時伺服器時候收到請求傳送的資料,根據請求行,請求頭,請求體進行解析。解析完成後返回響應行、響應頭、響應體。

注意:這裡伺服器返回狀態碼中有一些特殊的狀態碼

  • 301/302這兩個狀態碼都表示重定向,如果返回這兩個任意一個就會根據返回頭中的Location返回的域名重新進行上邊的一系列操作。

  • 304狀態碼錶示告訴瀏覽器本次資源走快取而不會重新請求下載資源。

這個過程便是一個最基礎的瀏覽器針對一個url訪問網路請求的過程。

taobao.com為例讓我們一探究竟

上邊說了那麼多枯燥的理論,接下來讓我們在實際中去體會一下。

首先我們開啟一個全新的瀏覽器tab頁在位址列輸入taobao.com

因為我是首次進入這個頁面,所以並沒有任何快取。前邊說到過瀏覽器程序首先會開啟一個頁面渲染程序,同時開啟網路程序去請求。

首先讓我們開啟chrome開發者工具:

image.png

有興趣的朋友可以自己嘗試輸入一下,這裡當我們輸入http://taobao.com/瀏覽器會解析DNS以及TCP三次握手建立連線然後傳送請求,當得到響應後發現Response Status302它會根據返回的Location重定向到http://www.taobao.com/

之後我們去重定向到http://www.taobao.com/,請求得到訪問又是301狀態碼,於是有被重新重定向https://taobao.com/上。

然後再次進行DNS解析,Tcp建立連線這個步驟。。

建議大家在新的無痕瀏覽頁中去進行這些操作,我們排除掉DNS快取以及任何瀏覽器快取的干擾機制去看結果會更加純粹。

這裡我們已經大概領略到了重定向域名的訪問,我們可以發現每一次重定向都會重新進行DNS解析以及TCP連線的建立是非常耗時的。所以在我們的真實專案中要儘量的避免進行資源重定向,如果有存在重定向的資源儘量還是將它直接替換成新的地址連線。

接下來我們以第三次https://www.taobao.com/這次請求為例來分析一下一次請求(無任何快取)的各個階段:

分析一次請求完整的瀑布圖所代表的含義

我們先來看看對應chrome中的瀑布圖:

image.png

  • Queueing 這個階段表示排隊階段,瀏覽器在以下情況下對請求進行排隊:
    • 有更高優先順序的請求。
    • 已經為此打開了六個 TCP 連線,這是限制。僅適用於 HTTP/1.0 和 HTTP/1.1。
    • 瀏覽器在磁碟快取中短暫分配空間
  • Stalled 表示停滯不前,請求可能因上述排隊中描述的任何原因而停止。(比如說連結開始後,會進行一些tcp連線的複用處理一些代理相關的邏輯)

  • DNS Lookup 這一步就表示開始進行DNS解析,將我們的請求域解析為ip地址。

  • Initial connection 這階段表示我們進行tcp連結/重試和ssl協商共同耗費的時間。

  • SSL 這一步就是當我們請求https域名時會進行ssl協商的耗時。

  • request sent 表示請求開始傳送

  • TTFB`` TTFB 代表 Time To First Byte。此時間包括 1 次往返延遲和伺服器準備響應所用的時間。通俗來說就是當我們請求傳送到接受到響應的第一個位元組的時間。

TTFB這一步通常可以粗略表示本次請求伺服器(後臺)從接受到請求然後返回響應結果處理的耗時。

  • Content Download 就不必多說了,是我們下載本地響應的時間。

同時對於chrome而言在http1.1下同一個域的最多支援併發6TCP連結,注意這裡是TCP連結而不是HTTP請求。

這裡因為1.1中引入了pipelining機制,客戶端可以同時傳送多個請求。這樣就進一步改進了HTTP協議的效率。

我們用一個小例子來說明下,在同一個TCP連線裡面,先發送A請求,然後等待伺服器做出迴應,收到後再發出B請求。管道機制則是允許瀏覽器同時發出A請求和B請求,但是伺服器還是按照順序,先回應A請求,完成後再回應B請求。

當然細心的同學會發現我們訪問的taobao.com基本上所有的靜態資源都是基於http2協議去實現的,這裡我們稍微來介紹一下http的各個階段:

http發展的各個階段

  • http 0.9 最早時候只支援傳輸html,請求中沒有任何請求頭。

  • http 1.0 引入了請求頭和響應頭,這樣的話就可以根據請求頭區分傳輸的內容是圖片還是html又或是js

  • http 1.1 針對http1.0每一次請求都會發送請求建立tcp連結,請求結束後斷開tcp連結。這無疑是非常耗時的。所以在http 1.1中預設開啟了一個請求頭connect:keep-alive進行在一個tcp連結的複用。當然即使引入了長連結keep-alive,還存在一個問題就是基於http 1.0中是一個請求傳送得到響應後才開始傳送下一個請求,針對這個機制1.1提出了管線化pipelining機制,但是需要注意的是伺服器對應同一tcp連結上的請求是一個一個去處理的,所以這就會導致一個比較嚴重的問題隊頭阻塞

如果說第一個傳送的請求丟包了,那麼伺服器會等待這個請求重新發送過來在進行返回處理。之後才會處理下一個請求。即使瀏覽器是基於pipelining去多個請求同時傳送的。

  • http 2.0 提出了很多個優化點,其中最著名的就是解決了http1.1中的隊頭阻塞問題。

    • 多路複用: 支援使用同一個tcp連結,基於二進位制分幀層進行傳送多個請求,支援同時傳送多個請求,同時伺服器也可以處理不同順序的請求而不必按照請每個請求的順序進行處理返回。這就解決了http 1.1中的隊頭阻塞問題。
    • 頭部壓縮: 在http2協議中對於請求頭進行了壓縮達到提交傳輸效能。
    • Server push: http2中支援通過服務端主動推送給客戶端對應的資源從而讓瀏覽器提前下載快取對應資源。
  • http3.0: 基於tcp下就難免存在阻塞問題,如果發生丟包就需要等待上一個包。在http3徹底解決了tcp的隊頭阻塞問題,它是基於udp協議並且在上層增加了一層QUIC協議。

關於http 3.02.0這部分我研究的不是很多,所以就不做詳細的對比了。大家如果有更詳細的建議可以在評論區留言。後續如果有必要我會補充這部分內容。

關於http 1.1pipelining機制和http 2.0的多路複用

其實這個問題最開始我也是一直困惑的,他們究竟存在什麼區別。直到有一天我看到了stackoverflow上這個答案

  • HTTP/1.1 without pipelining: 必須響應 TCP 連線上的每個 HTTP 請求,然後才能發出下一個請求。

  • HTTP/1.1 with pipelining: 可以立即發出 TCP 連線上的每個 HTTP 請求,而無需等待前一個請求的響應返回。響應將以相同的順序返回。

  • HTTP/2 multiplexing:  TCP 連線上的每個 HTTP 請求都可以立即發出,而無需等待先前的響應返回。響應可以按任何順序返回。

瀏覽器渲染

首先我們先來看一看關於瀏覽器載入的粗略載入圖

企業微信截圖_a510c17a-d3ed-4c56-984b-33866f82293c.png

粗略來說瀏覽器的渲染過程帶蓋就是這樣,但其實這其中涉及太多細節方面的知識點。比如一些檔案的載入順序,是否阻塞,CRP關鍵渲染路徑等等...

讓我們一層一層來揭開瀏覽器渲染的面紗。

cssjs對於dom的影響

關於cssjsdom構建的詳細分析,你可以在這裡看到

css是否會阻塞Dom

我們先來看看css對於dom的影響:

  1. 對於css的載入是不阻塞dom的構建的。
  2. 對於css的載入時會阻塞之後dom節點的渲染的。

關於如何理解這兩句話,我們結合這個Demo來看一看內容:

code.png

首先我們嘗試在chrome中將network網速調整到低網速情況下,你會發現頁面首先會打印出對應的id=app的節點但是此時頁面css並沒有渲染任何內容,等待css載入完成後頁面才會進行渲染

這樣也就意味著css的載入並不會阻塞Dom Tree的構建,但是css檔案的載入和解析是會阻塞頁面渲染的。

js是否會阻塞Dom

其實毋庸置疑,js的執行過程一定是會阻塞Dom TreeCss OM的。這裡有兩個特殊的asyncdefer屬性,我相信大家對於這兩個屬性都已經非常熟悉了。這裡我就不展開講解這部分了。

其實這裡大家只要把握一個原則,在渲染程序中JS執行緒和渲染執行緒是互斥的關係。

為什麼css放上邊而js放在下面

我們搞清楚了關於jscss阻塞的問題後再來看看一道經典的面試題:為什麼css放在上邊而js放在下面

為什麼css放在上邊

上邊我們講到了css的載入和解析並不會阻塞Dom的構建,但是會阻塞頁面上之後元素的渲染。這也就造成了如果css放在頂部的話,後續Dom元素的渲染需要依賴本次css程式碼執行解析完成之後才會

也許有的同學會想到,那如果我將css放在底部,是不是Dom元素首先會渲染出來之後等待樣式解析完成之後頁面又會重新進行一遍繪製,這樣的話使用者看來是不是"頁面展現"就更快了?

讓我們來看看這段程式碼:

code.png

Filmage-2021-10-21_140807.gif

我們可以看到將css放在底部的話頁面的確是會產生兩次渲染的。但是第一次沒有任何樣式的渲染其實是一次“無效渲染”。

同時讓我們來關注一下對比一次將css頂部造成的一次渲染和將css放在底部造成二次渲染的開銷:

image.png

我們利用chrome瀏覽器performance去分析將css放在底部的程式碼中發現實際上瀏覽器進行了兩次元素的繪製,也就是說如果將css程式碼放在底部是會發生重繪(以及可能會引發迴流),這個操作是非常耗時的一個過程。

關於重繪/迴流會在我們會在之後講到他們已經如何去儘量避免。

所以將css放在頂部的話:

頁面首次渲染瀏覽器僅僅會進行一次渲染,而不會造成多餘的重繪和迴流步驟。

為什麼js需要放在底部

上邊我們說到了關於js實際上是會阻塞Dom Tree的構建和渲染的。同時js依賴於前邊的css檔案載入完成後才會進行執行。

注意在網路程序中,解析html時候會提前針對所有外部連結進行預載入。簡單點來說也就是cssjs外部連結可以同時並行進行網路資源請求載入。

廢話不多說我們利用performance同樣來看這樣一段程式碼:

code.png

這裡我們將js放在了元素之前,首先在js執行完成之前是不會進行後續元素的構建和渲染的。只有等待js載入並且解析完成之後渲染執行緒才會繼續之後的Dom Tree的構建以及頁面的渲染。

js是會阻塞html解析和渲染的,同時需要注意js的執行是需要等待之前的css載入並且執行完畢。保證js可以操作樣式的。

所以css之後如果存在js那麼css的載入過程也是可以間接性的阻塞DCL事件的。

當然對於deferasync這兩個屬性我們會在後續深入講解。

這裡額外有一點:在頁面解析Html之前瀏覽器會額外掃描外部連結,將外部連結交給網路程序進行下載。所以cssjs的下載可以是並行的。

所以,我們之所以將js放在底部。是因為js放在底部是會等待頁面渲染完畢後再去阻塞的執行後續js

圖解cssjs的載入

image.png

  • css載入執行會阻塞後續js的執行,同時css載入會阻塞頁面的渲染。
  • css載入可能會阻塞後續dom解析,這需要根據後續是否存在js來判斷。
  • js載入和解析是會阻塞後續dom的解析。

寫在結尾

筆者原本打算從全方位的瀏覽器渲染流程講解再貫通到效能優化去逐步深入講解,但是寫到一半反觀關於效能和渲染方面的確涉及到的點是非常龐大的一個分支。

思來想去,這裡還是打算給大家拋磚引玉針對與渲染流程梳理一個大致的完整思路。後續會有不同的文章中去拆分出不同的細節去逐個攻破。

或者瀏覽器渲染上有哪一步部分比較感興趣,或者文章中的點還存在問題的話,可以評論區給我留言。