瀏覽器的渲染過程
在我們面試過程中,經(jīng)常會遇到面試官問我們娜遵,當我們從瀏覽器地址欄輸入URL之后到頁面顯示,瀏覽器到底發(fā)生了什么
下面從瀏覽器的角度告訴你久脯,在輸入URL后到按下回車跑慕,瀏覽器內(nèi)部發(fā)生了什么
目錄:
- 瀏覽器內(nèi)有哪些進程核行,這些進程的作用
- 瀏覽器地址輸入URL后,內(nèi)部的進程综苔,線程都做了哪些事
- 我們與瀏覽器交互時堡牡,內(nèi)部進程是怎么處理這些交互事件的
瀏覽器架構(gòu)
關于線程和進程
在介紹瀏覽器架構(gòu)之前,我們有必要先理解兩個概念可免,線程和進程
進程(process)是程序的一次執(zhí)行過程,是一個動態(tài)概念妇垢,是程序在執(zhí)行過程中分配和管理資源的基本單位。線程(thread)是CPU調(diào)度和分派的基本單位涨薪,它可與同屬一個進程的其他線程共享所擁有的全部資源。
簡單來說侠姑,進程可以看作一列列車,線程則是列車的車廂安吁。每節(jié)車廂之間是可以互通的柳畔。
而瀏覽器,屬于一個應用程序俘陷,而應用程序執(zhí)行,計算機就啟動了一個進程捉偏,進程啟動之后CPU會給該進程分配相應的內(nèi)存空間霞掺,當我們的進程得到了內(nèi)存之后,就可以使用線程進行資源調(diào)度骗灶。進而完成我們應用程序的功能。
而在應用程序中母廷,為了滿足功能的需求,啟動的進程會創(chuàng)建另外的新的進程來處理其他任務,這些創(chuàng)建出來的新的進程擁有全新的獨立的內(nèi)存空間舷暮,不能與原來的進程內(nèi)的內(nèi)存共享。他們之間的通訊需要通過IPC機制來進行沥割。
很多應用程序都采取這種多進程的形式凿菩,好處是可以讓進程與進程之間互相獨立机杜,互不影響。也就是可以有效的防止因為其中一個進程掛掉之后衅谷,影響到其他進程的執(zhí)行椒拗。
瀏覽器的多進程架構(gòu)
瀏覽器的架構(gòu),也是屬于這種多進程的應用程序,進程之間的通訊是利用IPC機制進行的蚀苛。我們下面以Chrome為例译红,介紹瀏覽器的多進程架構(gòu)
在Chrome中诗宣,主要的進程有4個:
瀏覽器進程(brower Process): 負責瀏覽器的TAB的前進忘古,后退,地址欄大年,書簽等工作。和處理瀏覽器的一些不可見的底層操作幸冻,比如網(wǎng)絡請求和文件訪問
渲染進程(render process)負責一個Tab內(nèi)的顯示相關的工作握侧,也稱渲染引擎
插件進程(plugin process)負責控制網(wǎng)頁使用到的插件
GPU進程(GPU process)負責處理整個應用程序的GPU任務
進程之間的關系:
首先外构,當我們要瀏覽一個網(wǎng)頁权烧,我們會在瀏覽器的地址欄里輸入URL券时,這個時候Browser Process會向這個URL發(fā)送請求,獲取這個URL的HTML內(nèi)容敦跌,然后將HTML內(nèi)容交給Renderer Process搂誉。
Render Process 解析HTML內(nèi)容父阻,解析遇到需要請求網(wǎng)絡的資源又返回回來交給Browser Process進行加載辑奈,同時通知Browser Process,需要plugin process加載插件資源星持,執(zhí)行插件代碼缠诅。
解析完成之后,Render Process計算得到圖像幀,并將這些圖像幀交給GPU process
GPU process將其轉(zhuǎn)化成為圖像顯示在屏幕上
多進程架構(gòu)的好處
- 更高的容錯性。多進程的架構(gòu)可以使得每一個渲染引擎允許在各自的進程中,相互之間不受影響晕换。也就是即使其中一個頁面奔潰掛掉了,其他頁面還能正常運行
- 更高的安全性和沙盒性斩狱。渲染引擎會經(jīng)常性的在網(wǎng)絡上遇到一些不可信任甚至是惡意的代碼改鲫。它們有可能會利用這些漏洞在你的電腦上安裝惡意軟件。針對這個問題瀏覽器可以對不同的進程限制不同權(quán)限焙糟,并未其提供沙盒運行環(huán)境。讓他更安全可靠
- 更高的響應速度名扛。多進程可以有效的規(guī)避多個任務相互競爭搶奪CPU資源的問題本涕。
多進程架構(gòu)優(yōu)化
根據(jù)上面的討論,我們現(xiàn)在已經(jīng)知道了瀏覽器的每一個Tab都有一個render process。而這些進程的內(nèi)存是不能共享的纽疟,但是有些時候不同的進程的內(nèi)存需要包含相同的內(nèi)容。針對這些情況Chrome瀏覽器提供了四種進程模式(process Models)來處理锈死。
- process-per-site-instance (默認) : 同一個 site-instance 使用同一個進程
- process-per-site : 同一個 site 使用一個進程
- process-per-tab : 每個tab使用一個進程
- single process : 所有tab使用同一個進程
補充:關于 site 和 site-instance
site 指的是相同的 registered domain name(如:google.com, baidu.com) 和 scheme(如:https://)缨该。就像a.baidu.com 和 b.baidu.com 就可以理解為同一個site
site-instance 指的是一組 connected pages from the same site 。
其中 connected 的含義是 can obtain references to each other in script code. 意思就是能否在腳本代碼中獲得彼此的引用川背。簡單來說就是滿足下面兩種情況并且打開的新頁面和舊頁面屬于上面定義的同一個site贰拿,就屬于同一個site-instance
- 用戶通過
<a target="_blank" >
這種方式點擊打開的頁面- js代碼打開的新頁面(比如window.open)
理解概念之后蛤袒,下面解釋四個進程模式
首先是Single process,顧名思義壮不,單進程模式汗盘,所有tab都會使用同一個進程。
接下來是process-per-tab询一,也是一樣隐孽,意思是每打開一個tab,都會新建一個進程
而process-per-site健蕊,當你打開a.baidu.com頁面菱阵,再打開b.baidu.com頁面的時候,這兩個tab都會共用同一個進程缩功。那么只要其中一個崩潰晴及,那么另一個也回崩潰
最后,也是最重要的Process-per-site-instance嫡锌,這個模式是chrome的默認使用模式虑稼,也就是幾乎所有的用戶都在使用的模式。當你打開一個tab訪問 a.baidu.com, 然后再打開一個tab訪問b.baidu.com势木,這兩個tab會使用兩個進程蛛倦。而如果在 a.baidu.com中,通過js代碼打開的b.baidu.com頁面啦桌,這兩個頁面使用同一個進程
默認模式選擇的原因
process-per-site-instance 兼容了性能與易用性溯壶,是一個比較中庸通用的模式
- 相較于 process-per-tab 能少開很多進程,意味著更少的內(nèi)存開銷
- 相較于 process-per-site 能夠更好的隔離相同域名下毫無相關的tab甫男,更安全
導航過程都發(fā)生了什么
下面我們開始深入了解進程和線程是如何呈現(xiàn)我們的網(wǎng)站頁面的
網(wǎng)頁加載過程
之前我們提到且改,tab以外的大部分工作都是由瀏覽器進程Browser process 負責,針對工作的不同板驳,Browser process劃分出不同的工作線程:
- UI thread : 控制瀏覽器上的按鈕及輸入框
- network thread : 處理網(wǎng)絡請求又跛,從網(wǎng)上獲取資源
- storage thread : 按鈕文件等訪問
第一步:處理輸入
當我們在瀏覽器的地址欄輸入內(nèi)容然后按回車時,UI thread會判斷輸入的內(nèi)容是否是搜索關鍵字若治,還是URL效扫。如果是關鍵字,跳轉(zhuǎn)到默認搜索引擎對應的搜索URL直砂。如果輸入的內(nèi)容是URL,則開始請求URL
第二步:開始導航
回車按下后浩习,UI thread將關鍵詞搜索對應的URL或輸入的的URL交給網(wǎng)絡線程Network thread静暂,此時UI線程使Tab前的圖標展示為加載中狀態(tài),然后網(wǎng)絡進程進行一系列DNS尋址谱秽,建立TLS連接等操作進行資源請求洽蛀,如果收到服務器的301重定向響應摹迷,它就會告知UI線程進行重定向然后再發(fā)起一個新的網(wǎng)絡請求。
第三步:讀取響應
network thread 接收到服務器響應后郊供,開始解析HTTP響應報文峡碉,然后根據(jù)響應頭中的Content-Type字段來確定響應主體的媒體類型,如果媒體類型是一個HTML文件驮审。則將響應數(shù)據(jù)交給渲染進程(render process)來進行下一步工作鲫寄,如果是zip文件或者其他文件,會把相關數(shù)據(jù)傳輸交給下載管理器
與此同時疯淫,瀏覽器會進程Safe Browsing安全檢查地来,如果域名或者請求內(nèi)容匹配到已知的惡意站點,network thread會展示一個警告頁熙掺。除此之外未斑,網(wǎng)絡線程還會做CORB(cross origin read block)檢查來確定那些敏感的跨站數(shù)據(jù),不會被發(fā)送至渲染進程的內(nèi)存中(這是為了提高攻擊者嘗試使用幽靈熔斷攻擊的成本的措施)
第四步:查找渲染進程
各種檢查完畢之后币绩,network thread 確信瀏覽器可以導航到請求網(wǎng)頁蜡秽, newtwork thread 會通知 UI thread 數(shù)據(jù)已經(jīng)準備好了, UI thread 會查找到一個 renderer process 進行頁面的渲染
瀏覽器為了對查找渲染進程這一步驟的優(yōu)化缆镣,考慮到網(wǎng)絡請求獲取響應需要時間芽突,所以在第二步開始,瀏覽器已經(jīng)預先查找和啟動了一個渲染進程费就,如果中間步驟一切順利诉瓦。當 network thread 接收到數(shù)據(jù)時,渲染進程已經(jīng)準備好了力细,但是如果遇到重定向睬澡,這個準備好的渲染進程也許就不可用了,這個時候會重新啟動一個渲染進程眠蚂。
第五步:提交導航
到了這一步煞聪,數(shù)據(jù)和渲染進程都準備好了, Browser process 會向 renderer process 發(fā)送IPC消息逝慧,來確定導航昔脯,此時,瀏覽器進程將準備好的數(shù)據(jù)發(fā)送給渲染進程笛臣,渲染進程接收到數(shù)據(jù)之后又發(fā)送IPC消息給瀏覽器進程云稚,告訴瀏覽器進程導航已提交,頁面開始加載
這個時候沈堡,導航欄會更新静陈,安全指示符,訪問歷史列表,即可以通過前進后退切換頁面了鲸拥。
第六步:初始化加載完成
當導航提交完成后拐格,渲染進程開始加載資源以及渲染頁面(下面介紹),當頁面渲染完成后(頁面及內(nèi)部的iframe都觸發(fā)了onload事件)刑赶,會向瀏覽器進程發(fā)送IPC消息捏浊,告知瀏覽器進程,這個時候UI thread會停止展示tab中的加載圖標
頁面渲染原理
導航過程完成之后撞叨,瀏覽器進程把數(shù)據(jù)交給了渲染進程金踪,渲染進程負責tab內(nèi)的所有事情,核心目的就是將HTML/CSS/JS代碼谒所,轉(zhuǎn)化為用戶可進行交互的web頁面热康。
渲染進程中,包含的線程分別是:
- 一個主線程(main thread)
- 多個工作線程(work thread)
- 一個合成器線程(compositor thread)
- 多個光柵化線程(raster thread)
不同的線程劣领,有著不同的工作職責
構(gòu)建DOM
當渲染進程接受倒導航的確認信息后姐军,開始接受來自瀏覽器進程的數(shù)據(jù),這個時候尖淘,主線程會解析數(shù)據(jù)轉(zhuǎn)化為DOM對象
DOM 為 web開發(fā)人員通過js與網(wǎng)頁進行交互的數(shù)據(jù)結(jié)構(gòu)API
資源子加載
在構(gòu)建DOM的過程中奕锌,會解析到圖片,CSS,JS腳本等資源村生,這些資源是需要從網(wǎng)絡或者緩存中獲取的惊暴,主線程在構(gòu)建DOM過程中如果遇到了這些資源,會逐一發(fā)起請求去獲取趁桃,而為了提升效率辽话,瀏覽器也會運行預加載掃描(preload scanner)程序,如果HTML中存在img卫病,link
等標簽油啤,預加載掃描程序就會把這些請求傳遞給Browser process的network thread進行資源下載
JavaScript的下載與執(zhí)行
構(gòu)建DOM過程中,如果遇到<script>
標簽蟀苛,渲染引擎會停止對HTML的解析益咬,而去加載執(zhí)行JS代碼,原因在于JS代碼可能會改變DOM的結(jié)構(gòu)(比如document.write()等API)
不過開發(fā)者其實也有多種方式來告知瀏覽器應該如何對待某個資源帜平,比如說如果在script
上添加async
和defer
等屬性幽告,瀏覽器會異步的加載和執(zhí)行js代碼,而不會阻塞渲染裆甩。
樣式計算
DOM樹只是我們頁面的結(jié)構(gòu)冗锁,我們要知道頁面長什么樣,我們還需要知道DOM的沒一個節(jié)點的樣式嗤栓。主線程在解析頁面時冻河,遇到style
標簽,或者是link
標簽的css資源,會加載css代碼芋绸,根據(jù)css代碼確定每個DOM節(jié)點的計算樣式
計算樣式是主線程根據(jù)css樣式選擇器計算出的每個DOM元素應該具備的具體樣式,即使你的頁面沒有設置任何自定義樣式担敌,瀏覽器也會提供默認的樣式
布局 - Layout
DOM樹和計算樣式完成后摔敛,我們還需要知道每一個節(jié)點在頁面上的位置,布局其實就是找到所有元素的幾何關系的過程全封。
主線程會遍歷DOM及相關元素的計算樣式马昙,構(gòu)建出包含每個元素的頁面坐標信息及盒子模型大小的布局樹(Render tree),遍歷過程中刹悴,會跳過隱藏的元素(display:none)行楞,另外,偽元素雖然在DOM上不可見土匀,但是在布局樹上是可見的子房。
繪制 - paint
布局 layout之后,我們知道了不同元素的結(jié)構(gòu)就轧,樣式证杭,幾何關系,我們要繪制出一個頁面妒御,我們要需要知道每個元素的繪制先后順序解愤,在繪制階段。主線程會遍歷布局樹(layout tree)乎莉,生成一系列的繪畫記錄(paint records)送讲。繪畫記錄可以看做是記錄各元素繪制先后順序的筆記。
合成 -compositing
文檔結(jié)構(gòu)惋啃,元素的樣式哼鬓,元素的幾何關系,繪畫順序肥橙,這些信息我們都有了魄宏,這個時候如果要繪制一個頁面,我們需要做的就是把這些信息轉(zhuǎn)化為顯示器中的像素存筏。這個轉(zhuǎn)化的過程宠互,叫做光柵化(rasterizing)
那我們要繪制一個頁面,最簡單的做法是只光柵化視窗內(nèi)的網(wǎng)頁內(nèi)容椭坚,如果用戶進行了頁面滾動予跌,就移動光柵幀(tastered frame)并且光柵化更多的內(nèi)容以補上頁面缺失的部分。
這種方式的缺點就是每當頁面滾動善茎,光柵線程都需要對新移進視圖的內(nèi)容進行光柵化券册,這是有一定的性能損耗的,為了優(yōu)化這種情況,chrome采用了一種更加復雜的方式合成(composition)
合成的意思就是烁焙,將頁面分成若干層航邢,然后分別對它們進行光柵化,最后在一個單獨的線程-合成線程(composition thread)里面合并成一個頁面的技術骄蝇。當用戶滾動頁面時膳殷,由于頁面各個層都已經(jīng)被光柵化了,瀏覽器需需要做的只是合成一個新的幀來展示滾動后的效果罷了九火。頁面的動畫實現(xiàn)也是類似赚窃,將頁面上的層進行移動并構(gòu)建出一個新的幀即可。
為了實現(xiàn)合成技術岔激,我們需要對元素進行分層勒极,確定哪些元素需要放置在哪一層,主線程需要遍歷渲染樹來創(chuàng)建一顆層次樹(layer tree)虑鼎,對于添加了 will-change
css屬性的元素辱匿,會被看作單獨一層,沒有 will-change
CSS屬性的元素震叙,瀏覽器會根據(jù)情況決定是否要把該元素放在單獨的層
你可能會想給頁面上所有的元素一個單獨的層掀鹅,然而當頁面的層超過一定數(shù)量后,層的合成操作要比在每個幀中光柵化頁面的一小部分還要慢媒楼,因此衡量你應用的渲染性能是是十分重要的一件事情乐尊。
一旦Layer tress被創(chuàng)建,渲染順序被確定划址。主線程會把這些信息通知給合成線程扔嵌,合成器線程開始對層次數(shù)的每一層進行光柵化。有的層的可以達到整個頁面的大小夺颤,所以合成線程需要將它們切分為一塊又一塊的小圖塊(tiles)痢缎,之后將這些小圖塊分別進行發(fā)送給一系列光柵化線程(raster thread)進行光柵化。結(jié)束后光柵化線程會將每個圖塊的光柵結(jié)果存在GPU Process的內(nèi)存中
為了優(yōu)化顯示體驗世澜,合成線程可以給不同的光柵線程賦予不同的優(yōu)先級独旷,將那些在視口中的或者視口附近的層先被光柵化。
當圖層上面的圖塊都被柵格化后寥裂,合成線程會收集圖塊上面叫做繪畫四邊形(draw quads)的信息來構(gòu)建一個合成幀(compositor frame)
- 繪畫四邊形:包含圖塊在內(nèi)存的位置以及圖層合成后圖塊在頁面的位置之類的信息
- 合成幀: 代表頁面一個幀的內(nèi)容的繪制四邊形集合
以上所有步驟完成后嵌洼,合成線程就會通過IPC向瀏覽器進程(browser process)提交一個渲染幀。這個時候可能有另外一個合成幀被瀏覽器進程的UI進程(UI thread)提交以改變?yōu)g覽器的UI封恰。這些合成幀都會被發(fā)送到GPU從而展示在屏幕上麻养。如果合成線程收到頁面滾動的事件,合成線程會構(gòu)建另外一個合成幀發(fā)送給GPU來更新頁面
合成的好處在于這個過程沒有涉及到主線程诺舔,所以合成線程不需要等待樣式的計算以及JavaScript完成執(zhí)行鳖昌。這就是為什么合成器相關的動畫最流程备畦。如果某個動畫涉及到布局或者繪制的調(diào)整,就會涉及到主線程的重新計算许昨,自然會慢很多
瀏覽器對事件的處理
當頁面渲染完畢以后懂盐,TAB內(nèi)已經(jīng)顯示出了可交互的WEB頁面,用戶可以進行移動鼠標糕档,點擊頁面等操作允粤。而當這些事件發(fā)生時候。瀏覽器會作出相應處理
以點擊事件為例翼岁。讓鼠標點擊頁面時,首先接受到事件信息的是 Browser process司光,但是browser process只知道事件發(fā)生的類型和發(fā)生的位置琅坡。具體怎么對這個點擊事件,進行處理還是由Tab內(nèi)的Render process進行的残家。Browser process接受倒事件后榆俺,隨后便把事件的信息傳遞給了渲染進程,渲染進程會找到根據(jù)事件發(fā)生的坐標坞淮,找到目標對象茴晋,并且運行這個目標對象的點擊事件綁定的監(jiān)聽函數(shù)
渲染進程中合成器線程接收事件
前面我們說到,合成線程可以獨立于主線程之外回窘,通過光柵化的層創(chuàng)建組合幀诺擅。例如頁面滾動,如果沒有對頁面滾動綁定相關事件啡直,組合器可以獨立于主線程創(chuàng)建組合幀烁涌,如果頁面綁定了頁面滾動事件,合成器線程會等待主線程進行事件處理后才會創(chuàng)建組合幀酒觅。那么撮执,合成器線程如何判斷出這個事件是否需要給主線程處理呢?
由于執(zhí)行js是主線的工作舷丹,當頁面合成時抒钱,合成器線程會標記頁面綁定有事件處理器的區(qū)域為非快速滾動區(qū)域,如果事件發(fā)生在這些存在標注的區(qū)域颜凯,合成器線程會把事件信息發(fā)送給主線程谋币,等待主線程進行事件處理。如果事件不是發(fā)生在這些區(qū)域装获,合成器線程則會直接合成新的幀而不用等到主線程的響應瑞信。
而對于非快速滾動區(qū)域的標記,開發(fā)者需要注意全局的事件綁定穴豫,比如我們使用事件委托凡简,將目標元素的事件交給根元素body進行處理
document.body.addEventListener('touchstart', evnet => {
if(event.target === area) {
event.prevenDefault()
}
})
在開發(fā)者角度看逼友,這一段代碼沒什么,但是從瀏覽器的角度看秤涩,這一段代碼給body元素綁定了事件監(jiān)聽器帜乞,也就意味著整個頁面都被編輯為一個非快速滾動區(qū),這會使得即使你的壓面的某些趨于沒有綁定任何事件筐眷,每次用戶觸發(fā)事件時黎烈,合成器線程也需要和主線程進行通信并等待反饋,流暢的合成器獨立處理合成幀的模式就失效了
其實這種情況也很好處理匀谣,只需要在事件監(jiān)聽時傳遞passtive
參數(shù)為 true照棋,passtive
會告訴瀏覽器你既要綁定事件,又要讓組合器線程直接跳過主線程的事件處理直接合成創(chuàng)建組合幀武翎。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});
查找事件的目標對象(event target)
當合成器線程接收到事件信息烈炭,判定到事件發(fā)生不在非快速滾動區(qū)域后,合成器線程會向主線程發(fā)送這個時間信息宝恶,主線程獲取到事件信息的第一件事就是通過命中測試(hit test)去找到事件的目標對象符隙。具體的命中測試流程是遍歷在繪制階段生成的繪畫記錄(paint records)來找到包含了事件發(fā)生坐標上的元素對象。
[站外圖片上傳中...(image-ea5ace-1607161505956)]
瀏覽器對事件的優(yōu)化
一般我們屏幕的幀率是每秒60幀垫毙,也就是60fps霹疫,但是某些事件觸發(fā)的頻率超過了這個數(shù)值,比如wheel综芥,mousewheel丽蝎,mousemove,pointermove膀藐,touchmove征峦,這些連續(xù)性的事件一般每秒會觸發(fā)60~120次,假如每一次觸發(fā)事件都將事件發(fā)送到主線程處理消请,由于屏幕的刷新速率相對來說較低栏笆,這樣使得主線程會觸發(fā)過量的命中測試以及JS代碼,使得性能有了沒必要是損耗臊泰。
出于優(yōu)化的目的蛉加,瀏覽器會合并這些連續(xù)的事件,延遲到下一幀渲染是執(zhí)行缸逃,也就是requestAnimationFrame
之前针饥。
而對于非連續(xù)性的事件,如keydown需频,keyup丁眼,mousedown,mouseup昭殉,touchstart苞七,touchend等藐守,會直接派發(fā)給主線程去執(zhí)行
總結(jié)
瀏覽器的多進程架構(gòu),根據(jù)不同功能劃分了不同進程蹂风,進程內(nèi)不同的使命劃分了不同的線程卢厂,當用戶開始瀏覽網(wǎng)頁時候,瀏覽器進程進行處理輸入惠啄,開始導航請求數(shù)據(jù)慎恒,請求響應數(shù)據(jù),請求響應數(shù)據(jù)撵渡,查找新建渲染進程融柬,提交導航,之后渲染又進行了解析HTML構(gòu)建DOM趋距,構(gòu)建過程加載子資源丹鸿,下載并執(zhí)行JS代碼,樣式計算棚品,布局,繪制廊敌,合成铜跑,一步一步的構(gòu)建出一個可交互的WEB頁面,之后瀏覽器進程又接受頁面的交互事件信息骡澈,并將其交給渲染進程锅纺,渲染進程內(nèi)主進程進行命中測試,查找到目標元素并執(zhí)行綁定的事件肋殴,完成頁面交互囤锉。
以上