天天看點

【浏覽器】769- 你不知道的浏覽器渲染原理

【浏覽器】769- 你不知道的浏覽器渲染原理

作者:_楊溜溜

在我們面試過程中,面試官經常會問到這麼一個問題,那就是從在浏覽器位址欄中輸入URL到頁面顯示,浏覽器到底發生了什麼?這個問題看起來是老生常談,但是這個問題回答的好壞,确實可以很好的反映出面試者知識的廣度和深度。

本文從浏覽器角度來告訴你,URL後輸入後按回車,浏覽器内部究竟發生了什麼,讀完本文後,你将了解到:

  • 浏覽器内有哪些程序,這些程序都有些什麼作用
  • 浏覽器位址輸入URL後,内部的程序、線程都做了哪些事
  • 我們與浏覽器互動時,内部程序是怎麼處理這些互動事件的

浏覽器架構

在講浏覽器架構之前,先了解兩個概念,​

​程序​

​和​

​線程​

​。

程序(process)是程式的一次執行過程,是一個動态概念,是程式在執行過程中配置設定和管理資源的基本機關,線程(thread)是CPU排程和分派的基本機關,它可與同屬一個程序的其他的線程共享程序所擁有的全部資源。

簡單的說呢,程序可以了解成正在執行的應用程式,而線程呢,可以了解成我們應用程式中的代碼的執行器。而他們的關系可想而知,線程是跑在程序裡面的,一個程序裡面可能有一個或者多個線程,而一個線程,隻能隸屬于一個程序。

大家都知道,浏覽器屬于一個應用程式,而應用程式的一次執行,可以了解為計算機啟動了一個​

​程序​

​,程序啟動後,CPU會給該程序配置設定相應的記憶體空間,當我們的程序得到了記憶體之後,就可以使用​

​線程​

​進行資源排程,進而完成我們應用程式的功能。

而在應用程式中,為了滿足功能的需要,啟動的程序會建立另外的新的程序來處理其他任務,這些建立出來的新的程序擁有全新的獨立的記憶體空間,不能與原來的程序内向記憶體,如果這些程序之間需要通信,可以通過IPC機制(Inter Process Communication)來進行。

【浏覽器】769- 你不知道的浏覽器渲染原理

很多應用程式都會采取這種多程序的方式來工作,因為程序和程序之間是互相獨立的它們​

​互不影響​

​,也就是說,當其中一個程序挂掉了之後,不會影響到其他程序的執行,隻需要重新開機挂掉的程序就可以恢複運作。

浏覽器的多程序架構

假如我們去開發一個浏覽器,它的架構可以是一個單程序多線程的應用程式,也可以是一個使用IPC通信的多程序應用程式。

不同的浏覽器使用不同的架構,下面主要以Chrome為例,介紹浏覽器的多程序架構。

在Chrome中,主要的程序有4個:

  • 浏覽器程序 (Browser Process):負責浏覽器的TAB的前進、後退、位址欄、書簽欄的工作和處理浏覽器的一些不可見的底層操作,比如網絡請求和檔案通路。
  • 渲染程序 (Renderer Process):負責一個Tab内的顯示相關的工作,也稱渲染引擎。
  • 插件程序 (Plugin Process):負責控制網頁使用到的插件
  • GPU程序 (GPU Process):負責處理整個應用程式的GPU任務
【浏覽器】769- 你不知道的浏覽器渲染原理

這4個程序之間的關系是什麼呢?

首先,當我們是要浏覽一個網頁,我們會在浏覽器的位址欄裡輸入URL,這個時候​

​Browser Process​

​會向這個URL發送請求,擷取這個URL的HTML内容,然後将HTML交給​

​Renderer Process​

​,​

​Renderer Process​

​解析HTML内容,解析遇到需要請求網絡的資源又傳回來交給​

​Browser Process​

​進行加載,同時通知​

​Browser Process​

​,需要​

​Plugin Process​

​加載插件資源,執行插件代碼。解析完成後,​

​Renderer Process​

​計算得到圖像幀,并将這些圖像幀交給​

​GPU Process​

​,​

​GPU Process​

​将其轉化為圖像顯示螢幕。

【浏覽器】769- 你不知道的浏覽器渲染原理

多程序架構的好處

Chrome為什麼要使用多程序架構呢?

第一,更高的容錯性。當今WEB應用中,HTML,JavaScript和CSS日益複雜,這些跑在渲染引擎的代碼,頻繁的出現BUG,而有些BUG會直接導緻渲染引擎崩潰,多程序架構使得每一個渲染引擎運作在各自的程序中,互相之間不受影響,也就是說,當其中一個頁面崩潰挂掉之後,其他頁面還可以正常的運作不收影響。

【浏覽器】769- 你不知道的浏覽器渲染原理

第二,更高的安全性和沙盒性(sanboxing)。渲染引擎會經常性的在網絡上遇到不可信、甚至是惡意的代碼,它們會利用這些漏洞在你的電腦上安裝惡意的軟體,針對這一問題,浏覽器對不同程序限制了不同的權限,并為其提供沙盒運作環境,使其更安全更可靠

第三,更高的響應速度。在單程序的架構中,各個任務互相競争搶奪CPU資源,使得浏覽器響應速度變慢,而多程序架構正好規避了這一缺點。

多程序架構優化

之前的我們說到,​

​Renderer Process​

​的作用是負責一個Tab内的顯示相關的工作,這就意味着,一個Tab,就會有一個Renderer Process,這些程序之間的記憶體無法進行共享,而不同程序的記憶體常常需要包含相同的内容。

浏覽器的程序模式

為了節省記憶體,Chrome提供了四種程序模式(Process Models),不同的程序模式會對 tab 程序做不同的處理。

  • Process-per-site-instance (default) - 同一個 site-instance
  • Process-per-site - 同一個 site
  • Process-per-tab -
  • Single process -

這裡需要給出 site 和 site-instance 的定義

  • 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使用的是共一個程序,因為這兩個頁面的site相同,而如此一來,如果其中一個tab崩潰了,而另一個tab也會崩潰。

​Process-per-site-instance​

​兩個程序。而如果你在 a.baidu.com 中,通過JS代碼打開了 b.baidu.com 頁面,這兩個 tab 會使用同一個程序。

預設模式選擇

那麼為什麼浏覽器使用​

​Process-per-site-instance​

​作為預設的程序模式呢?

​Process-per-site-instance​

​相容了性能與易用性,是一個比較中庸通用的模式。

  • 相較于 Process-per-tab,能夠少開很多程序,就意味着更少的記憶體占用
  • 相較于 Process-per-site,能夠更好的隔離相同域名下毫無關聯的 tab,更加安全

導航過程都發生了什麼

前面我們講了浏覽器的多程序架構,講了多程序架構的各種好處,和Chrome是怎麼優化多程序架構的,下面從使用者浏覽網頁這一簡單的場景,來深入了解程序和線程是如何呈現我們的網站頁面的。

網頁加載過程

之前我們我們提到,tab以外的大部分工作由浏覽器程序​

​Browser Process​

​負責,針對工作的不同,Browser Process 劃分出不同的工作線程:

  • UI thread:控制浏覽器上的按鈕及輸入框;
  • network thread:處理網絡請求,從網上擷取資料;
  • storage thread:控制檔案等的通路;
【浏覽器】769- 你不知道的浏覽器渲染原理

第一步:處理輸入

當我們在浏覽器的位址欄輸入内容按下回車時,​

​UI thread​

​會判斷輸入的内容是搜尋關鍵詞(search query)還是URL,如果是搜尋關鍵詞,跳轉至預設搜尋引擎對應都搜尋URL,如果輸入的内容是URL,則開始請求URL。

【浏覽器】769- 你不知道的浏覽器渲染原理

第二步:開始導航

回車按下後,​

​UI thread​

​将關鍵詞搜尋對應的URL或輸入的URL交給網絡線程​

​Network thread​

​,此時UI線程使Tab前的圖示展示為加載中狀态,然後網絡程序進行一系列諸如DNS尋址,建立TLS連接配接等操作進行資源請求,如果收到伺服器的301重定向響應,它就會告知UI線程進行重定向然後它會再次發起一個新的網絡請求。

【浏覽器】769- 你不知道的浏覽器渲染原理

第三步:讀取響應

​network thread​

​接收到伺服器的響應後,開始解析HTTP響應封包,然後根據響應頭中的​

​Content-Type​

​字段來确定響應主體的媒體類型(MIME Type),如果媒體類型是一個HTML檔案,則将響應資料交給渲染程序(renderer process)來進行下一步的工作,如果是 zip 檔案或者其它檔案,會把相關資料傳輸給下載下傳管理器。

與此同時,浏覽器會進行 Safe Browsing 安全檢查,如果域名或者請求内容比對到已知的惡意站點,network thread 會展示一個警告頁。除此之外,網絡線程還會做 CORB(Cross Origin Read Blocking)檢查來确定那些敏感的跨站資料不會被發送至渲染程序。

第四步:查找渲染程序

各種檢查完畢以後,network thread 确信浏覽器可以導航到請求網頁,network thread 會通知 UI thread 資料已經準備好,UI thread 會查找到一個 renderer process 進行網頁的渲染。

【浏覽器】769- 你不知道的浏覽器渲染原理

浏覽器為了對查找渲染程序這一步驟進行優化,考慮到網絡請求擷取響應需要時間,是以在第二步開始,浏覽器已經預先查找和啟動了一個渲染程序,如果中間步驟一切順利,當 network thread 接收到資料時,渲染程序已經準備好了,但是如果遇到重定向,這個準備好的渲染程序也許就不可用了,這個時候會重新啟動一個渲染程序。

第五步:送出導航

到了這一步,資料和渲染程序都準備好了,​

​Browser Process​

​ 會向 ​

​Renderer Process ​

​發送IPC消息來确認導航,此時,浏覽器程序将準備好的資料發送給渲染程序,渲染程序接收到資料之後,又發送IPC消息給浏覽器程序,告訴浏覽器程序導航已經送出了,頁面開始加載。

【浏覽器】769- 你不知道的浏覽器渲染原理

這個時候導航欄會更新,安全訓示符更新(位址前面的小鎖),通路曆史清單(history tab)更新,即可以通過前進後退來切換該頁面。

第六步:初始化加載完成

當導航送出完成後,渲染程序開始加載資源及渲染頁面(詳細内容下文介紹),當頁面渲染完成後(頁面及内部的iframe都觸發了onload事件),會向浏覽器程序發送IPC消息,告知浏覽器程序,這個時候UI thread會停止展示tab中的加載中圖示。

網頁渲染原理

導航過程完成之後,浏覽器程序把資料交給了渲染程序,渲染程序負責tab内的所有事情,核心目的就是将HTML/CSS/JS代碼,轉化為使用者可進行互動的web頁面。那麼渲染程序是如何工作的呢?

渲染程序中,包含線程分别是:

  • 一個主線程(main thread)
  • 多個工作線程(work thread)
  • 一個合成器線程(compositor thread)
  • 多個光栅化線程(raster thread)
【浏覽器】769- 你不知道的浏覽器渲染原理

不同的線程,有着不同的工作職責。

建構DOM

當渲染程序接受到導航的确認資訊後,開始接受來自浏覽器程序的資料,這個時候,主線程會解析資料轉化為DOM(Document Object Model)對象。

DOM為WEB開發人員通過JavaScript與網頁進行互動的資料結構及API。

資源子加載

在建構DOM的過程中,會解析到圖檔、CSS、JavaScript腳本等資源,這些資源是需要從網絡或者緩存中擷取的,主線程在建構DOM過程中如果遇到了這些資源,逐一發起請求去擷取,而為了提升效率,浏覽器也會運作預加載掃描(preload scanner)程式,如果如果HTML中存在​

​img​

​、​

​link​

​等标簽,預加載掃描程式會把這些請求傳遞給​

​Browser Process​

​的network thread進行資源下載下傳。

【浏覽器】769- 你不知道的浏覽器渲染原理

JavaScript的下載下傳與執行

建構DOM過程中,如果遇到​

​<script>​

​标簽,渲染引擎會停止對HTML的解析,而去加載執行JS代碼,原因在于JS代碼可能會改變DOM的結構(比如執行​

​document.write()​

​等API)

不過開發者其實也有多種方式來告知浏覽器應對如何應對某個資源,比如說如果在​

​<script>​

​ 标簽上添加了 ​

​async​

​ 或 ​

​defer​

​ 等屬性,浏覽器會異步的加載和執行JS代碼,而不會阻塞渲染。

樣式計算 - Style calculation

DOM樹隻是我們頁面的結構,我們要知道頁面長什麼樣子,我們還需要知道DOM的每一個節點的樣式。主線程在解析頁面時,遇到​

​<style>​

​标簽或者​

​<link>​

​标簽的CSS資源,會加載CSS代碼,根據CSS代碼确定每個DOM節點的計算樣式(computed style)。

計算樣式是主線程根據CSS樣式選擇器(CSS selectors)計算出的每個DOM元素應該具備的具體樣式,即使你的頁面沒有設定任何自定義的樣式,浏覽器也會提供其預設的樣式。

【浏覽器】769- 你不知道的浏覽器渲染原理

布局 - Layout

DOM樹和計算樣式完成後,我們還需要知道每一個節點在頁面上的位置,布局(Layout)其實就是找到所有元素的幾何關系的過程。

主線程會周遊DOM 及相關元素的計算樣式,建構出包含每個元素的頁面坐标資訊及盒子模型大小的布局樹(Render Tree),周遊過程中,會跳過隐藏的元素(display: none),另外,僞元素雖然在DOM上不可見,但是在布局樹上是可見的。

【浏覽器】769- 你不知道的浏覽器渲染原理

繪制 - Paint

布局 layout 之後,我們知道了不同元素的結構,樣式,幾何關系,我們要繪制出一個頁面,我們要需要知道每個元素的繪制先後順序,在繪制階段,主線程會周遊布局樹(layout tree),生成一系列的繪畫記錄(paint records)。繪畫記錄可以看做是記錄各元素繪制先後順序的筆記。

【浏覽器】769- 你不知道的浏覽器渲染原理

合成 - Compositing

文檔結構、元素的樣式、元素的幾何關系、繪畫順序,這些資訊我們都有了,這個時候如果要繪制一個頁面,我們需要做的是把這些資訊轉化為顯示器中的像素,這個轉化的過程,叫做​

​光栅化​

​(rasterizing)。

那我們要繪制一個頁面,最簡單的做法是隻光栅化視口内(viewport)的網頁内容,如果使用者進行了頁面滾動,就移動光栅幀(rastered frame)并且光栅化更多的内容以補上頁面缺失的部分,如下:

【浏覽器】769- 你不知道的浏覽器渲染原理

Chrome第一個版本就是采用這種簡單的繪制方式,這一方式唯一的缺點就是每當頁面滾動,光栅線程都需要對新移進視圖的内容進行光栅化,這是一定的性能損耗,為了優化這種情況,Chrome采取一種更加複雜的叫做合成(compositing)的做法。

那麼,什麼是合成?合成是一種将頁面分成若幹層,然後分别對它們進行光栅化,最後在一個單獨的線程 - 合成線程(compositor thread)裡面合并成一個頁面的技術。當使用者滾動頁面時,由于頁面各個層都已經被光栅化了,浏覽器需要做的隻是合成一個新的幀來展示滾動後的效果罷了。頁面的動畫效果實作也是類似,将頁面上的層進行移動并建構出一個新的幀即可。

【浏覽器】769- 你不知道的浏覽器渲染原理

為了實作合成技術,我們需要對元素進行分層,确定哪些元素需要放置在哪一層,主線程需要周遊渲染樹來建立一棵層次樹(Layer Tree),對于添加了 ​

​will-change​

​ CSS 屬性的元素,會被看做單獨的一層,沒有 ​

​will-change​

​ CSS屬性的元素,浏覽器會根據情況決定是否要把該元素放在單獨的層。

【浏覽器】769- 你不知道的浏覽器渲染原理

你可能會想要給頁面上所有的元素一個單獨的層,然而當頁面的層超過一定的數量後,層的合成操作要比在每個幀中光栅化頁面的一小部分還要慢,是以衡量你應用的渲染性能是十分重要的一件事情。

一旦Layer Tress被建立,渲染順序被确定,主線程會把這些資訊通知給合成器線程,合成器線程開始對層次數的每一層進行光栅化。有的層的可以達到整個頁面的大小,是以合成線程需要将它們切分為一塊又一塊的小圖塊(tiles),之後将這些小圖塊分别進行發送給一系列光栅線程(raster threads)進行光栅化,結束後光栅線程會将每個圖塊的光栅結果存在​

​GPU Process​

​的記憶體中。

【浏覽器】769- 你不知道的浏覽器渲染原理

為了優化顯示體驗,合成線程可以給不同的光栅線程賦予不同的優先級,将那些在視口中的或者視口附近的層先被光栅化。

當圖層上面的圖塊都被栅格化後,合成線程會收集圖塊上面叫做繪畫四邊形(draw quads)的資訊來建構一個合成幀(compositor frame)。

  • 繪畫四邊形:包含圖塊在記憶體的位置以及圖層合成後圖塊在頁面的位置之類的資訊。
  • 合成幀:代表頁面一個幀的内容的繪制四邊形集合。

以上所有步驟完成後,合成線程就會通過IPC向浏覽器程序(browser process)送出(commit)一個渲染幀。這個時候可能有另外一個合成幀被浏覽器程序的UI線程(UI thread)送出以改變浏覽器的UI。這些合成幀都會被發送給GPU進而展示在螢幕上。如果合成線程收到頁面滾動的事件,合成線程會建構另外一個合成幀發送給GPU來更新頁面。

【浏覽器】769- 你不知道的浏覽器渲染原理

合成的好處在于這個過程沒有涉及到主線程,是以合成線程不需要等待樣式的計算以及JavaScript完成執行。這就是為什麼合成器相關的動畫最流暢,如果某個動畫涉及到布局或者繪制的調整,就會涉及到主線程的重新計算,自然會慢很多。

浏覽器對事件的處理

當頁面渲染完畢以後,TAB内已經顯示出了可互動的WEB頁面,使用者可以進行移動滑鼠、點選頁面等操作了,而當這些事件發生時候,浏覽器是如何處理這些事件的呢?

以點選事件(click event)為例,讓滑鼠點選頁面時候,首先接受到事件資訊的是​

​Browser Process​

​,但是Browser Process隻知道事件發生的類型和發生的位置,具體怎麼對這個點選事件進行處理,還是由Tab内的​

​Renderer Process​

​進行的。Browser Process接受到事件後,随後便把事件的資訊傳遞給了渲染程序,渲染程序會找到根據事件發生的坐标,找到目标對象(target),并且運作這個目标對象的點選事件綁定的監聽函數(listener)。

【浏覽器】769- 你不知道的浏覽器渲染原理

渲染程序中合成器線程接收事件

前面我們說到,合成器線程可以獨立于主線程之外通過已光栅化的層建立組合幀,例如頁面滾動,如果沒有對頁面滾動綁定相關的事件,組合器線程可以獨立于主線程建立組合幀,如果頁面綁定了頁面滾動事件,合成器線程會等待主線程進行事件處理後才會建立組合幀。那麼,合成器線程是如何判斷出這個事件是否需要路由給主線程處理的呢?

由于執行 JS 是主線程的工作,當頁面合成時,合成器線程會标記頁面中綁定有事件處理器的區域為​

​非快速滾動區域​

​(non-fast scrollable region),如果事件發生在這些存在标注的區域,合成器線程會把事件資訊發送給主線程,等待主線程進行事件處理,如果事件不是發生在這些區域,合成器線程則會直接合成新的幀而不用等到主線程的響應。

【浏覽器】769- 你不知道的浏覽器渲染原理

而對于非快速滾動區域的标記,開發者需要注意全局事件的綁定,比如我們使用事件委托,将目标元素的事件交給根元素body進行處理,代碼如下:

document.body.addEventListener('touchstart', event => {
if (event.target === area) {
    event.preventDefault()
  }
})      

在開發者角度看,這一段代碼沒什麼問題,但是從浏覽器角度看,這一段代碼給body元素綁定了事件監聽器,也就意味着整個頁面都被編輯為一個非快速滾動區域,這會使得即使你的頁面的某些區域沒有綁定任何事件,每次使用者觸發事件時,合成器線程也需要和主線程通信并等待回報,流暢的合成器獨立處理合成幀的模式就失效了。

【浏覽器】769- 你不知道的浏覽器渲染原理

其實這種情況也很好處理,隻需要在事件監聽時傳遞​

​passtive​

​參數為 true,​

​passtive​

​會告訴浏覽器你既要綁定事件,又要讓組合器線程直接跳過主線程的事件處理直接合成建立組合幀。

document.body.addEventListener('touchstart', event => {
if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});      

查找事件的目标對象(event target)

當合成器線程接收到事件資訊,判定到事件發生不在非快速滾動區域後,合成器線程會向主線程發送這個時間資訊,主線程擷取到事件資訊的第一件事就是通過命中測試(hit test)去找到事件的目标對象。具體的命中測試流程是周遊在繪制階段生成的繪畫記錄(paint records)來找到包含了事件發生坐标上的元素對象。

浏覽器對事件的優化

一般我們螢幕的幀率是每秒60幀,也就是60fps,但是某些事件觸發的頻率超過了這個數值,比如wheel,mousewheel,mousemove,pointermove,touchmove,這些連續性的事件一般每秒會觸發60~120次,假如每一次觸發事件都将事件發送到主線程處理,由于螢幕的重新整理速率相對來說較低,這樣使得主線程會觸發過量的命中測試以及JS代碼,使得性能有了沒必要是損耗。

【浏覽器】769- 你不知道的浏覽器渲染原理

出于優化的目的,浏覽器會合并這些連續的事件,延遲到下一幀渲染是執行,也就是​

​requestAnimationFrame​

​之前。

【浏覽器】769- 你不知道的浏覽器渲染原理

而對于非連續性的事件,如keydown,keyup,mousedown,mouseup,touchstart,touchend等,會直接派發給主線程去執行。

總結

浏覽器的多程序架構,根據不同的功能劃分了不同的程序,程序内不同的使命劃分了不同的線程,當使用者開始浏覽網頁時候,浏覽器程序進行處理輸入、開始導航請求資料、請求響應資料,查找建立渲染程序,送出導航,之後渲染又進行了解析HTML建構DOM、建構過程加載子資源、下載下傳并執行JS代碼、樣式計算、布局、繪制、合成,一步一步的建構出一個可互動的WEB頁面,之後浏覽器程序又接受頁面的互動事件資訊,并将其交給渲染程序,渲染程序内主程序進行命中測試,查找目标元素并執行綁定的事件,完成頁面的互動。

本文大部分内容也是對inside look at modern web browser系列文章的整理、解讀和翻譯吧,整理過程還是收獲非常大的,希望讀者讀了本文隻有有所啟發吧。

相關參考連結

  • 為什麼浏覽器會使用多程序架構
  • 一文看懂Chrome浏覽器工作原理
  • 浏覽器多程序架構
  • 圖解浏覽器的基本工作原理
  • Inside look at modern web browser (part 2)
  • Inside look at modern web browser (part 3)

繼續閱讀