文章內容較多,某些未涉及內容,可結合我的另一篇文章(http://www.itdecent.cn/p/f8ae03a295a2)一起看,有意外收獲.
一、進程和線程
進程是操作系統(tǒng)資源分配的最小單位,進程包含線程
線程是受進程管理的,瀏覽器采用的是多進程模式
日常中我們使用瀏覽器是基于一個一個tab頁來進行訪問網站,如果說某一個tab頁掛掉了,對于其他tab頁是沒有任何影響的,說明每一個tab頁都是一個單獨的進程,他們之間相互獨立,互不影響.
瀏覽器中的進程:

瀏覽器中的進程分為5個:
1.瀏覽器進程:你可以理解瀏覽器進程是一個統(tǒng)一的‘調度大師‘,去調度其他進程,比如我們在地址欄中輸入url時,瀏覽器進程首先會調度網絡進程,他可以做一些子進程管理以及一些存儲的處理.
2.渲染進程:這個進程對于我們來說是最重要的一個進程,每一個tab頁都有一個獨立的渲染進程,他的主要作用是渲染頁面.
3.網絡進程:這個進程是控制對于一些靜態(tài)資源的請求,它將資源請求完成之后交給渲染進程進行渲染.
4.GPU進程:這個進程可以調用硬件進行渲染,從而實現(xiàn)渲染加速.比如translate3d等css3屬性會騙取調用GPU進程從而開啟硬件加速.
5.插件進程:chrome中的插件也是一個獨立的進程
各個進程之間是相互獨立,互不影響的.
從輸入url到頁面顯示之間究竟發(fā)生了什么?
?http://www.itdecent.cn/p/f8ae03a295a2
二、網絡資源層面
首先我們先拋開瀏覽器對于資源的處理過程,先來看看一次正常的url輸入在資源加載方面經歷的生命周期.
當我們在地址欄中輸入一個url時,瀏覽器進程會監(jiān)聽到這次交互.緊接著它會分配一個渲染進程準備渲染頁面,同時瀏覽器進程會調用網絡進程加載資源.
等網絡進程加載完資源后會將資源交給渲染進程進行頁面渲染.從進程的角度來說整體的加載流程就是這樣.
大的方面來說就是瀏覽器進程進行調度,網絡進程加載完資源后交給渲染進程進行渲染加載的資源.
接下來我們詳細看看輸入url之后的請求過程中究竟發(fā)生了哪些事情.
網絡七層協(xié)議

我們可以將這七層歸為下列四層:
應用層:通常我們會將應用層、表示層、會話層統(tǒng)稱為應用層,應用層的主要協(xié)議是http協(xié)議.
傳輸層:傳輸層中我們?yōu)g覽器中http協(xié)議是基于tcp去進行網絡傳輸.(常見傳輸協(xié)議的有tcp還有udp)
網絡層:網絡層中一般都是ip協(xié)議.
物理層:當然在數(shù)據(jù)鏈路層和物理層都被稱為物理層.
我們先從7層協(xié)議來分析一下瀏覽器對于url加載的過程
首先當我們輸入url輸入一個域名,瀏覽器會在磁盤/內存緩存中去查找請求的文件,查找是否命中緩存.如果命中緩存則直接會從緩存中拿到對應的ip地址.
如果命中緩存則會直接返回對應資源不會進入下面的步驟
瀏覽器緩存:http://www.itdecent.cn/p/ccb9c60354a8
這里我們先忽略緩存帶來的影響,這里涉及一個協(xié)商緩存和強制緩存的知識點在下面的知識點中進行講解.
假設我們首次訪問這個頁面,此時并沒有任何緩存:
如果我們訪問的這個域名沒有被解析過,那么我們需要解析地址欄中輸入的域名.解析域名主要依賴的是DNS協(xié)議,將域名解析為ip地址. ip地址才能找到域名對應的ip.
dns你可以理解它為一個映射表,將域名和ip地址進行映射,其實就是一個分布式的數(shù)據(jù)庫,通過域名查找到對應的ip地址.
需要注意的是dns解析是基于udp協(xié)議而非tcp協(xié)議.
這里有個小問題需要提一下,為什么dns解析時基于udp而非tcp協(xié)議?
我們的dns解析過程是一個服務器的查找過程.因為域名分為一級/二級...域名,所以每一級域名都會迭代去查詢,如果它采用tcp協(xié)議的話,每經過一次域名查詢,域名服務器都會經過三次握手.但是udp就不會,他會直接發(fā)包然后確認
相較于udp,tcp是更加安全,可靠的(因為三次握手以及四次揮手)但是這也造成了它相較于udp消耗更多時間.
udp常用的場景是視頻或者直播中,對于我們來說dns解析中使用的udp更多的原因是因為udp的速度,當然即使丟包了,我們重新發(fā)送就可以了.
tcp傳輸?shù)倪^程稱為分段傳輸,也就是會拆分為多個包,一個包一個包的進行發(fā)送得到響應之后就會發(fā)送下一個包.這樣的方式無疑更加可靠和安全,但是在實效上并不如udp協(xié)議的實時(直接通信無需建立連接)
此時會根據(jù)dns解析通過域名+端口號解析出對應的ip地址.
我們擁有了ip地址之后,接下來我們就需要將利用ip進行尋找網頁地址
此時如果我們的請求地址時候https,在通過ip尋址之后會額外增加一步ssl協(xié)商保證數(shù)據(jù)的安全性
當IP地址尋址成功后,瀏覽器知道了服務器的地址,此時并不會立即將數(shù)據(jù)發(fā)送過去,而是會進入一個排隊的等待過程,比如一個域名下有多個請求,同一個域名在http1.1下最多只建立6個tcp鏈接,也就說同一個時間最多發(fā)送6個請求,他們首先會進入一個排隊的等待時間.
排隊結束后開始發(fā)送請求.此時就需要通過tcp先進性創(chuàng)建鏈接通過三次握手,建立完成鏈接后傳輸數(shù)據(jù).
上邊我們說過tcp是基于分段傳輸?shù)?基于內容特別大的傳輸內容tcp會將數(shù)據(jù)包進行拆分成為多個數(shù)據(jù)包進行有序傳輸.
在tcp的傳輸過程中如果傳輸中出現(xiàn)了丟包,那么tcp會進行重傳.
服務器接收到之后會按照順序進行接收.
tcp建立完鏈接之后,瀏覽器會通過http請求發(fā)送請求數(shù)據(jù)
一次http請求包含
請求行
請求頭
請求體
在http1.1中默認開啟了connection:keep-alive,它的作用是在下次發(fā)送請求時在一定時間內可以復用上一次的tcp鏈接而不需要重新建立鏈接.(也就是在一定時間內保持相同域名tcp鏈接不斷開).
此時服務器接收到請求發(fā)送的數(shù)據(jù),根據(jù)請求行、請求頭、請求體進行解析,解析完成后返回相應行、響應頭、響應體.
注意:這里服務器返回狀態(tài)碼中有一些特殊的狀態(tài)碼
301/302這兩個狀態(tài)碼都是表示重定向,如果返回這兩個任意一個,就會根據(jù)響應頭中的location返回的域名重新進行之前操作(https://blog.csdn.net/qq_43968080/article/details/107355758)
304狀態(tài)碼表示瀏覽器本次資源走緩存而不會重新請求下載資源.
這個過程便是一個最基礎的瀏覽器針對一個url訪問網絡請求的過程。
以taobao.com為例讓我們一探究竟
上邊說了那么多枯燥的理論,接下來讓我們在實際中去體會一下。
首先我們打開一個全新的瀏覽器tab頁在地址欄輸入taobao.com
因為我是首次進入這個頁面,所以并沒有任何緩存。前邊說到過瀏覽器進程首先會開啟一個頁面渲染進程,同時開啟網絡進程去請求。
首先讓我們打開chrome開發(fā)者工具:

建議大家在新的無痕瀏覽頁中去進行這些操作,我們排除掉DNS緩存以及任何瀏覽器緩存的干擾機制去看結果會更加純粹。
每一次重定向都會重新進行DNS解析以及TCP連接的建立是非常耗時的。所以在我們的真實項目中要盡量的避免進行資源重定向,如果有存在重定向的資源盡量還是將它直接替換成新的地址連接。
接下來我們以第三次https://www.taobao.com/這次請求為例來分析一下一次請求(無任何緩存)的各個階段:
分析一次請求完整的瀑布圖所代表的含義
我們先來看看對應chrome中的瀑布圖:

Queueing?這個階段表示排隊階段,瀏覽器在以下情況下對請求進行排隊:
有更高優(yōu)先級的請求。
已經為此源打開了六個 TCP 連接,這是限制。僅適用于 HTTP/1.0 和 HTTP/1.1。
瀏覽器在磁盤緩存中短暫分配空間
Stalled?表示停滯不前,請求可能因上述排隊中描述的任何原因而停止。(比如說鏈接開始后,會進行一些tcp連接的復用處理一些代理相關的邏輯)
DNS Lookup?這一步就表示開始進行DNS解析,將我們的請求域解析為ip地址。
Initial connection?這階段表示我們進行tcp鏈接/重試和ssl協(xié)商共同耗費的時間。
SSL?這一步就是當我們請求https域名時會進行ssl協(xié)商的耗時。
request sent?表示請求開始發(fā)送
watting for server代表?Time To First Byte。此時間包括 1 次往返延遲和服務器準備響應所用的時間。通俗來說就是當我們請求發(fā)送到接受到響應的第一個字節(jié)的時間。
waitting for server?這一步通??梢源致员硎颈敬握埱蠓掌?后臺)從接受到請求然后返回響應結果處理的耗時。
Content Download?就不必多說了,是我們下載本地響應的時間。
同時對于chrome而言在http1.1下同一個域的最多支持并發(fā)6個TCP鏈接,注意這里是TCP鏈接而不是HTTP請求。
我們用一個小例子來說明下,在同一個TCP連接里面,先發(fā)送A請求,然后等待服務器做出回應,收到后再發(fā)出B請求。管道機制則是允許瀏覽器同時發(fā)出A請求和B請求,但是服務器還是按照順序,先回應A請求,完成后再回應B請求(https://www.zhihu.com/pin/1681622411215228928?utm_id=0)。
http發(fā)展的各個階段
http 0.9
? ?最早時候只支持傳輸html,請求中沒有任何請求頭。
http 1.0?
? ? 引入了請求頭和響應頭,這樣的話就可以根據(jù)請求頭區(qū)分傳輸?shù)膬热菔菆D片還是html又或是js。
http 1.1?
? ? ?針對http1.0每一次請求都會發(fā)送請求建立tcp鏈接,請求結束后斷開tcp鏈接。這無疑是非常耗時的。所以在http 1.1中默認開啟了一個請求頭connect:keep-alive進行在一個tcp鏈接的復用。當然即使引入了長鏈接keep-alive,還存在一個問題就是基于http 1.0中是一個請求發(fā)送得到響應后才開始發(fā)送下一個請求,針對這個機制1.1提出了管線化pipelining機制,但是需要注意的是服務器對應同一tcp鏈接上的請求是一個一個去處理的,所以這就會導致一個比較嚴重的問題隊頭阻塞。
如果說第一個發(fā)送的請求丟包了,那么服務器會等待這個請求重新發(fā)送過來在進行返回處理。之后才會處理下一個請求。即使瀏覽器是基于pipelining去多個請求同時發(fā)送的。
http 2.0
? ? ?提出了很多個優(yōu)化點,其中最著名的就是解決了http1.1中的隊頭阻塞問題。
? ? ? ?HTTP/2復用TCP連接則不同,雖然依然遵循請求-響應模式,但客戶端發(fā)送多個請求和服務端給出多個響應的順序不受限制,這樣既避免了"隊頭堵塞",又能更快獲取響應。在復用同一個TCP連接時,服務器同時(或先后)收到了A、B兩個請求,先回應A請求,但由于處理過程非常耗時,于是就發(fā)送A請求已經處理好的部分, 接著回應B請求,完成后,再發(fā)送A請求剩下的部分。HTTP/2長連接可以理解成全雙工的協(xié)議
http 2.0 的特點:
多路復用: 支持使用同一個tcp鏈接,基于二進制分幀層進行發(fā)送多個請求,支持同時發(fā)送多個請求,同時服務器也可以處理不同順序的請求而不必按照請每個請求的順序進行處理返回。這就解決了http 1.1中的隊頭阻塞問題。
頭部壓縮: 在http2協(xié)議中對于請求頭進行了壓縮達到提交傳輸性能。
Server push:?http2中支持通過服務端主動推送給客戶端對應的資源從而讓瀏覽器提前下載緩存對應資源。
http3.0:
? ? ?基于tcp下就難免存在阻塞問題,如果發(fā)生丟包就需要等待上一個包。在http3徹底解決了tcp的隊頭阻塞問題,它是基于udp協(xié)議并且在上層增加了一層QUIC協(xié)議。
關于http 3.0和2.0這部分我研究的不是很多,所以就不做詳細的對比了。大家如果有更詳細的建議可以在評論區(qū)留言。后續(xù)如果有必要我會補充這部分內容。
關于http 1.1的pipelining機制和http 2.0的多路復用,他們究竟存在什么區(qū)別?
HTTP/1.0 without pipelining: 必須響應 TCP 連接上的每個 HTTP 請求,然后才能發(fā)出下一個請求。HTTP/1.1 with pipelining: 可以立即發(fā)出 TCP 連接上的每個 HTTP 請求,而無需等待前一個請求的響應返回。響應將以相同的順序返回。
HTTP/2 multiplexing:?TCP 連接上的每個 HTTP 請求都可以立即發(fā)出,而無需等待先前的響應返回??煞謳劝凑枕樞蚍祷靥幚砗玫牟糠謨热?,接著返回下一個請求等等,在返回之前處理好沒返回的內容
Summary:HTTP/1.1 最大的變化就是引入了持久連接(persistent connection),在HTTP/1.1中默認開啟 Connection: keep-alive,即TCP連接默認不關閉,可以被多個請求復用。
? ? ? ? ? 那么管道機制就是在同一個TCP連接中可以同時發(fā)送多個HTTP請求而不用等待上一個請求返回數(shù)據(jù)后,再發(fā)送下一個請求。雖然可以同時發(fā)送多個HTTP請求,但是服務器響應是按照請求的順序進行響應的。
? ? ? ? ? HTTP 2.0的多路復用是在同一個TCP連接中,可以發(fā)送多個HTTP請求,而且請求的響應不依賴于前一個請求。每個請求單獨處理,不會出現(xiàn)HTTP1.1中上一個請求沒有回應便一直等待的情況。
三、瀏覽器渲染
首先我們先來看一看關于瀏覽器加載資源的粗略圖
參考?https://zhuanlan.zhihu.com/p/575565899
1、渲染進程
? ? ? 渲染進程有GUI線程、js引擎線程、定時器觸發(fā)線程、異步HTTP請求線程、事件觸發(fā)線程,參考下圖了解每個線程的作用,以及各個線程相互協(xié)作完成渲染的過程。

2、GUI線程的工作


渲染過程描述
?解析html以構建DOM樹 -> 構建render樹 ->布局render樹 -> 繪制render樹
? 首先,當瀏覽器拿到html文檔時首先會進行HTML文檔解析,構建DOM樹
? 其次,遇到css樣式如link標簽或者style標簽時會解析css,構建樣式樹
HTML解析構建和css的解析構建是相互獨立的并不會造成沖突,因此我們通常將css樣式放在head中,讓瀏覽器盡早解析css
再次,當html的解析遇到scrip標簽,就會停止DOM樹的解析,開始下載js
因為js是會堵塞html解析的,是堵塞資源,其原因在于js可能會改變html現(xiàn)有結構,將控制權移交給javascript引擎;等javascript引擎運行完畢,瀏覽器會從中斷的地方恢復DOM構建。而因此就會推遲頁面首繪的時間。可以在首繪不需要js的情況下用async和defer實現(xiàn)異步加載,這樣js就不會阻塞html的解析了。
注意:異步執(zhí)行是指下載。執(zhí)行js時仍然會堵塞。當html解析完成之后,瀏覽器會將文檔標注為交互狀態(tài),并開始解析那些處于defferred模式的腳本,也就是那些應該在文檔解析完后才執(zhí)行的腳本。然后,文檔狀態(tài)將設置為完成,一個加載事件將隨之觸發(fā)。
再一次,在得到DOM樹和樣式樹后就可以進行渲染樹的構建了
應該注意的是渲染樹和DOM元素相對應的,但并非一一對應,比如非可視化的DOM元素不會插入呈現(xiàn)樹中,例如‘head’元素,如果元素的display屬性為‘none’,那么也不會顯示在呈現(xiàn)樹中(但是visibility屬性值為hidden的元素仍會顯示)
再一次,渲染樹構建完畢后將會進行布局
布局使用流模型的layout算法。所謂流模型即是指Layout的過程只需要進行一遍即可。但實際實現(xiàn)中,流模型會有例外,Layout是一個遞歸的過程,每個節(jié)點都負責自己及其子節(jié)點的Layout。Layout結果是相對父節(jié)點的坐標和尺寸。其過程可以簡述為:
(1)父節(jié)點確定自己的寬度
(2)父節(jié)點完成子節(jié)點放置,確定其相對坐標?
?(3)節(jié)點確定自己的寬度和高度
(4)父節(jié)點根據(jù)所有的子結點高度
最后,render Tree已經構建完成
瀏覽器渲染樹引擎并不直接使用渲染樹進行繪制,為了方便處理定位(裁剪),溢出滾動(頁內滾動),css轉換/不透明/濾鏡,蒙版或反射,Z(Z排序)等,瀏覽器需要生成另外一棵樹 - 層樹。
因此繪制過程如下:
? ? ?獲取DOM并將其分割為多個層(RenderLayer)將每個層柵格化,并獨立的繪制進位圖中將這些位圖作為文理上傳至GPU復合多個層來生成最終的屏幕圖像(終極layer)
3、問題知識點匯總?
(1)關于瀏覽器渲染的容易誤解點總結
1、DOM樹的構建是邊加載邊解析的.
? ? ? DOM樹的構建是從接收到文檔開始的,一邊會將字節(jié)轉化為字符,字符轉化為標記,標記構建dom樹
? ? ? 這個構成被分為標記化和構建化
? ? ? 這是個漸進的過程,為達到更好的用戶體驗,呈現(xiàn)引擎會力求盡快將內容顯示在屏幕上。它不必等到整個HTML文檔解析完畢之后,就會開始構建呈現(xiàn)樹和設置布局。在不斷接收和處理來自網絡的其余內容的同時,呈現(xiàn)引擎會將部分內容解析并顯示出來。
參考文檔:http://taligarsiel.com/Projects/howbrowserswork1.htm2、渲染樹的構建是一邊加載,一邊解析,一邊渲染?
? ? ? 這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊加載,一邊解析,一邊渲染的工作現(xiàn)象。參考文檔:http://www.itdecent.cn/p/2d522fc2a8f83、css的標簽嵌套越多,越不容易定位到元素
? ? ? css的解析是自右至左逆向解析的,嵌套越多越增加瀏覽器的工作量,而不會越快。
? ? ?因為如果正向解析,例如「div div p em」,我們首先就要檢查當前元素到 html 的整條路徑,找到最上層的 div,再往下找,如果遇到不匹配就必須回到最上層那個 div,往下再去匹配選擇器中的第一個 div,回溯若干次才能確定匹配與否,效率很低。
? ? ? 逆向匹配則不同,如果當前的 DOM 元素是 div,而不是 selector 最后的 em,那只要一步就能排除。只有在匹配時,才會不斷向上找父節(jié)點進行驗證。打個比如 p span.showing
? ? ? ?你認為從一個p元素下面找到所有的span元素并判斷是否有class showing快,還是找到所有的span元素判斷是否有class showing并且包括一個p父元素快
(2) css與js對于dom的影響
? ? ? ?js加載和解析是會阻塞后續(xù)dom的解析。
? ? ? ?css加載會阻塞頁面的渲染。
? ? ? ?css加載不會阻塞后續(xù)dom解析(https://blog.csdn.net/qq_41462647/article/details/130161709)
? ? ? ?css加載會阻塞后續(xù)js的執(zhí)行
css是否會阻塞Dom?
1.對于css的加載是不阻塞dom的構建的。
2.對于css的加載時會阻塞之后的dom節(jié)點的渲染的(需要把樣式加載完之后再渲染)。js是否會阻塞Dom?
其實毋庸置疑,js的執(zhí)行過程一定是會阻塞Dom Tree和Css OM的。
其實這里大家只要把握一個原則,在渲染進程中JS線程和渲染線程是互斥的關系。
為什么css放上邊而js放在下面?
上邊我們講到了css的加載和解析并不會阻塞Dom的構建,但是會阻塞頁面上之后元素的渲染。這也就造成了如果css放在頂部的話,后續(xù)Dom元素的渲染需要依賴本次css代碼執(zhí)行解析完成之后才會渲染。
將css放在底部的話頁面的確是會產生兩次渲染的。但是第一次沒有任何樣式的渲染其實是一次“無效渲染”。
我們利用chrome瀏覽器performance去分析將css放在底部的代碼中發(fā)現(xiàn)實際上瀏覽器進行了兩次元素的繪制,也就是說如果將css代碼放在底部是會發(fā)生重繪(以及可能會引發(fā)回流),這個操作是非常耗時的一個過程。
所以將css放在頂部的話:頁面首次渲染瀏覽器僅僅會進行一次渲染,而不會造成多余的重繪和回流步驟。
為什么js需要放在底部?
上邊我們說到了關于js實際上是會阻塞Dom Tree的構建和渲染的。同時js依賴于前邊的css文件加載完成后才會進行執(zhí)行。保證js可以操作樣式的。所以css之后如果存在js那么css的加載過程也是可以間接性的阻塞DCL事件的。
將js放在了元素之前,首先在js執(zhí)行完成之前是不會進行后續(xù)元素的構建和渲染的。只有等待js加載并且解析完成之后渲染線程才會繼續(xù)之后的Dom Tree的構建以及頁面的渲染
這里額外有一點:在頁面解析Html之前瀏覽器會額外掃描外部鏈接,將外部鏈接交給網絡進程進行下載。所以css和js的下載可以是并行的。所以,我們之所以將js放在底部。是因為js放在底部是會等待頁面渲染完畢后再去執(zhí)行后續(xù)js。
defer和async這兩個屬性的不同
參考:https://juejin.cn/post/6894629999215640583
? ? ? ? ??https://zhuanlan.zhihu.com/p/622763093
參考:https://zhuanlan.zhihu.com/p/458664335