瀏覽器工做原理

架構

進程和線程

進程能夠被描述爲是一個應用的執行程序。javascript

線程存在於進程並執行程序任意部分。css

啓動應用時會建立一個進程。程序也許會建立一個或多個線程來幫助它工做,這是可選的。html

操做系統爲進程提供了一個可使用的「一塊」內存,全部應用程序狀態都保存在該私有內存空間中。html5

關閉應用程序時,相應的進程也會消失,操做系統會釋放內存。java

進程能夠請求操做系統啓動另外一個進程來執行不一樣的任務。git

此時,內存中的不一樣部分會分給新進程。若是兩個進程須要對話,他們能夠經過進程間通訊IPC)來進行。github

許多應用都是這樣設計的,因此若是一個工做進程失去響應,該進程就能夠在不中止應用程序不一樣部分的其餘進程運行的狀況下從新啓動。web

瀏覽器架構

那麼如何經過進程和線程構建 web 瀏覽器呢?它可能由一個擁有不少線程的進程,或是一些經過 IPC 通訊的不一樣線程的進程。算法

在2016年,Chrome官方團隊使用「面向服務的架構」(Services Oriented Architecture,簡稱SOA)的思想設計了新的Chrome架構。瀏覽器

若是在資源受限的設備上(以下圖),Chrome會將不少服務整合到一個進程中,從而節省內存佔用。

進程 控制
瀏覽器進程 主要負責界面顯示、用戶交互、子進程管理,同時提供存儲等功能。
渲染進程 核心任務是將 HTML、CSS 和 JavaScript 轉換爲用戶能夠與之交互的網頁,排版引擎Blink和JavaScript引擎V8都是運行在該進程中,默認狀況下,Chrome會爲每一個Tab標籤建立一個渲染進程。出於安全考慮,渲染進程都是運行在沙箱模式下。
插件進程 主要是負責插件的運行,因插件易崩潰,因此須要經過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面形成影響。
網絡進程 主要負責頁面的網絡資源加載,以前是做爲一個模塊運行在瀏覽器進程裏面的,直至最近才獨立出來,成爲一個單獨的進程。
GPU進程 在獨立的進程中處理GPU任務。之因此放到獨立的進程,是由於GPU要處理來自多個應用的請求,但要在同一個界面上繪製圖形。

SOA架構的優勢

  1. 穩定且流暢

    因爲進程是相互隔離的,因此當一個頁面或者插件崩潰時,影響到的僅僅是當前的頁面進程或者插件進程,並不會影響到瀏覽器和其餘頁面,這就完美地解決了頁面或者插件的崩潰會致使整個瀏覽器崩潰,也就是不穩定的問題。

    同理,JavaScript也是運行在渲染進程中的,因此即便JavaScript阻塞了渲染進程,影響到的也只是當前的渲染頁面,而並不會影響瀏覽器和其餘頁面。

  2. 安全沙箱

    SOA架構還有助於安全和隔離。由於操做系統有限制進程特權的機制,瀏覽器能夠藉此限制某些進程的能力。

    好比,Chrome會限制處理任意用戶輸入的渲染器進程,不讓它任意訪問文件。

  3. 更內聚,鬆耦合,易於維護和擴展

    原來的各類模塊被重構成獨立的服務(Service),每一個服務(Service)均可以在獨立的進程中運行,訪問服務(Service)必須使用定義好的接口,經過IPC來通訊,從而構建一個更內聚、鬆耦合、易於維護和擴展的系統,更好實現 Chrome 簡單、穩定、高速、安全的目標。

站點隔離

站點隔離(Site Isolation for web developers)是新近引入Chrome的一個里程碑式特性,即每一個跨站點iframe都運行一個獨立的渲染器進程。

即使像前面說的那樣,每一個標籤頁單開一個渲染器進程,但容許跨站點的iframe運行在同一個渲染器進程中並共享內存空間,那安全攻擊仍然有可能繞開同源策略,並且有人發如今現代CPU中,進程有可能讀取任意內存( Meltdown/Spectre )。

進程隔離是隔離站點、確保上網安全最有效的方式。

Chrome 默認採用站點隔離。站點隔離是多年工程化努力的結果,它並不是多開幾個渲染器進程那麼簡單。

好比,不一樣的iframe運行在不一樣進程中,開發工具在後臺仍然要作到無縫切換,並且即使簡單地Ctrl+F查找也會涉及在不一樣進程中搜索。

導航

導航涉及瀏覽器進程與線程間爲顯示網頁而通訊。一切從用戶在瀏覽器中輸入一個URL開始。輸入URL以後,瀏覽器會經過互聯網獲取數據並顯示網頁。從請求網頁到瀏覽器準備渲染網頁的過程,叫作導航。

下面咱們逐步看一看導航的幾個步驟。

第一步:處理輸入

UI線程會判斷用戶輸入的是查詢字符串仍是URL。由於Chrome的地址欄同時也是搜索框。

第二步:開始導航

若是輸入的是URL,首先,網絡進程會查找本地緩存是否緩存了該資源。

若是有緩存資源,那麼直接返回資源給瀏覽器進程。

若是在緩存中沒有查找到資源,那麼UI線程會通知網絡線程發起網絡調用,獲取網站內容。

此時標籤頁左端顯示旋轉圖標,網絡線程進行DNS查詢、創建TLS鏈接(對於HTTPS)。

網絡線程可能收到服務器的重定向頭部,如HTTP 301。此時網絡線程會跟UI線程溝通,告訴它服務器要求重定向。而後,再發起對另外一個URL的請求。

第三步:讀取響應

服務器返回的響應體到來以後,網絡線程會檢查接收到的前幾個字節。響應的Content-Type頭部應該包含數據類型,若是沒有這個字段,則須要MIME類型嗅探

若是響應是HTML文件,那下一步就是把數據交給渲染器進程。但若是是一個zip文件或其餘文件,那就意味着是一個下載請求,須要把數據傳給下載管理器。

此時也是「安全瀏覽」檢查的環節。若是域名和響應數據匹配已知的惡意網站,網絡線程會顯示警告頁。

此外,CORB (Cross Origin Read Blocking) 檢查也會執行,以確保敏感的跨站點數據不會發送給渲染器進程。

第四步:聯繫渲染器進程

全部查檢完畢,網絡線程確認瀏覽器能夠導航到用戶請求的網站,因而會通知UI線程數據已經準備好了。UI線程會聯繫渲染器進程渲染網頁。

打開一個新頁面採用的渲染進程策略:

  • 一般狀況下,打開新的頁面都會使用單獨的渲染進程;
  • 若是從A頁面打開B頁面,且A和B都屬於同一站點的話,那麼B頁面複用A頁面的渲染進程;若是是其餘狀況,瀏覽器進程則會爲B建立一個新的渲染進程。

    • 具體地講,咱們將「同一站點」定義爲根域名(例如,baidu.com)加上協議(例如,https:// 或者http://),還包含了該根域名下的全部子域名和不一樣的端口。

      https://WWW.baidu.com
      https://WWW.baidu.com:8080
因爲網絡請求可能要花幾百毫秒才能拿到響應,這裏還會應用一個優化策略。第二步UI線程要求網絡線程發送請求後,已經知道可能要導航到哪一個網站去了。所以在發送網絡請求的同時,UI線程會提早聯繫或並行啓動一個渲染器進程。這樣在網絡線程收到數據後,就已經有渲染器進程原地待命了。若是發生了重定向,這個待命進程可能用不上,而是換做其餘進程去處理。

第五步:提交導航

數據和渲染器進程都有了,就能夠經過IPC從瀏覽器進程向渲染器進程提交導航。渲染器進程也會同時接收到不間斷的HTML數據流。

當瀏覽器進程收到渲染器進程的確認消息後,導航完成,文檔加載階段開始。

此時,地址欄會更新,安全指示圖標和網站設置UI也會反映新頁面的信息。

當前標籤頁面的會話歷史會更新,後退/前進按鈕起做用。爲便於標籤頁/會話在關閉標籤頁或窗口後恢復,會話歷史會寫入磁盤。

最後一步:初始加載完成

提交導航以後,渲染器進程將負責加載資源和渲染頁面(具體細節後面介紹)。

而在「完成」渲染後(在全部iframe中的onload事件觸發且執行完成後),渲染器進程會經過IPC給瀏覽器進程發送一個消息。此時,UI線程中止標籤頁上的旋轉圖標。

初始加載完成後,客戶端JavaScript仍然可能加載額外資源並從新渲染頁面。

若是此時用戶在地址又輸入了其餘URL呢?瀏覽器進程還會重複上述步驟,導航到新站點。不過在此以前,須要確認已渲染的網站是否關注beforeunload事件。由於標籤頁中的一切,包括JavaScript代碼都由渲染器進程處理,因此瀏覽器進程必須與當前的渲染器進程確認後再導航到新站點。

若是導航請求來自當前渲染器進程(用戶點擊了連接或JavaScript運行了window.location = "https://newsite.com"),渲染器進程首先會檢查beforeunload處理程序。而後,它會走一遍與瀏覽器進程觸發導航一樣的過程。惟一的區別在於導航請求是由渲染器進程提交給瀏覽器進程的。

導航到不一樣的網站時,會有一個新的獨立渲染器進程負責處理新導航,而老的渲染器進程要負責處理unload之類的事件。

更多細節,能夠參考【譯】頁面生命週期API以及Web 頁面生命週期

另外,導航階段還可能涉及【中】Service Worker,即網頁應用中的網絡代理服務,開發者能夠經過它控制什麼緩存在本地,什麼時候從網絡獲取新數據。

Service Worker說到底也是須要渲染器進程運行的JavaScript代碼。若是網站註冊了Server Worker,那麼導航請求到來時,網絡線程會根據URL將其匹配出來,此時UI線程就會聯繫一個渲染器進程來執行Service Worker的代碼:可能只要從本地緩存讀取數據,也可能須要發送網絡請求。

若是Service Worker最終決定從網絡請求數據,瀏覽器進程與渲染器進程間的這種往返通訊會致使延遲。

所以,這裏會有一個「導航預加載」的優化Speed up Service Worker with Navigation Preloads,即在Service Worker啓動同時預先加載資源,加載請求經過HTTP頭部與服務器溝通,服務器決定是否徹底更新內容。

渲染

渲染是渲染器進程內部的工做,涉及Web性能的諸多方面(詳細內容能夠參考這裏Why does speed matter?)。

標籤頁中的一切都由渲染器進程負責處理,其中主線程負責運行大多數客戶端JavaScript代碼,少許代碼可能會由工做線程處理(若是用到了Web Worker或Service Worker)。

合成器(compositor)線程和柵格化(raster)線程負責高效、平滑地渲染頁面。

渲染器進程的核心任務是把HTML、CSS和JavaScript轉換成用戶能夠交互的網頁接下來,咱們從總體上過一遍渲染器進程處理Web內容的各個階段。

解析HTML

構建DOM

渲染器進程收到導航的提交消息後,開始接收HTML,其主線程開始解析文本字符串(HTML),並將它轉換爲DOM(Document Object Model,文檔對象模型)。

DOM是瀏覽器內部對頁面的表示,也是JavaScript與之交互的數據結構和API。

如何將HTML解析爲DOM由HTML標準定義。HTML標準要求瀏覽器兼容錯誤的HTML寫法,所以瀏覽器會「忍氣吞聲」,毫不報錯。詳情能夠看看「解析器錯誤處理及怪異情形簡介」。

加載子資源

網站都會用到圖片、CSS和JavaScript等外部資源。瀏覽器須要從緩存或網絡加載這些文件。主線程能夠在解析並構建DOM的過程當中發現一個加載一個,但這樣效率過低。

爲此,Chrome會在解析同時併發運行「預加載掃描器」,當發現HTML文檔中有<img><link>時,預加載掃描器會將請求提交給瀏覽器進程中的網絡線程。

JavaScript可能阻塞解析

若是HTML解析器碰到<script>標籤,會暫停解析HTML文檔並加載、解析和執行JavaScript代碼。

由於JavaScript有可能經過document.write()修改文檔,進而改變DOM結構(HTML標準的「解析模型」有一張圖能夠一目瞭然)。因此HTML解析器必須停下來執行JavaScript,而後再恢復解析HTML。至於執行JavaScript的細節,你們能夠關注V8團隊相關的分享:【譯】 JavaScript 引擎基礎:Shapes 和 Inline Caches

提示瀏覽器你要加載資源

爲了更好地加載資源,能夠經過不少方式告訴瀏覽器。若是JavaScript沒有用到document.write(),能夠在<script>標籤上添加asyncdefer屬性。這樣瀏覽器就會異步運行JavaScript代碼,不會阻塞解析。合適的話,能夠考慮使用JavaScript modules。再好比,<link rel="preload">告訴瀏覽器該資源對於當前導航絕對必要,應該儘快下載。關於資源加載優先級,能夠參考這裏:【譯】Fast load times

計算樣式(Recalculate style)

光有DOM還不行,由於並不知道頁面應該長啥樣。因此接下來,主線程要解析CSS並計算每一個DOM節點的樣式。這個過程就是根據CSS選擇符,肯定每一個元素要應用什麼樣式。在Chrome開發工具「計算的樣式」(computed)中能夠看每一個元素計算後的樣式。

1.把CSS轉換爲瀏覽器可以理解的結構

CSS樣式來源主要有三種:

  • 經過link引用的外部CSS文件;
  • <style>標記內的 CSS;
  • 元素的style屬性內嵌的CSS。
  • 和HTML文件同樣,瀏覽器也是沒法直接理解這些純文本的CSS樣式,因此當渲染引擎接收到CSS文本時,會執行一個轉換操做,將CSS文本轉換爲瀏覽器能夠理解的結構——styleSheets
  • 爲了加深理解,你能夠在Chrome控制檯中查看其結構,只須要在控制檯中輸入document.styleSheets,而後就看到以下圖所示的結構, 該數據結構同時具有了查詢和修改功能。

2.轉換樣式表中的屬性值,使其標準化

3.計算出DOM樹中每一個節點的具體樣式

這就涉及到CSS的繼承規則和層疊規則了。

首先是CSS繼承。

  • 首先,能夠選擇要查看的元素的樣式(位於圖中的區域2中),在圖中的第1個區域中點擊對應的元素元素,就能夠了下面的區域查看該元素的樣式了。好比這裏咱們選擇的元素是<p>標籤,位於html.body.div.這個路徑下面。
  • 其次,能夠從樣式來源(位於圖中的區域3中)中查看樣式的具體來源信息,看看是來源於樣式文件,仍是來源於UserAgent樣式表。這裏須要特別提下UserAgent樣式,它是瀏覽器提供的一組默認樣式,若是你不提供任何樣式,默認使用的就是UserAgent樣式。
  • 最後,能夠經過區域2和區域3來查看樣式繼承的具體過程。

樣式計算過程當中的第二個規則是樣式層疊。層疊是CSS的一個基本特徵,它是一個定義瞭如何合併來自多個源的屬性值的算法

佈局(Layout)

到這一步,渲染器進程知道了文檔的結構,也知道了每一個節點的樣式。但基於這些信息仍然不足以渲染頁面。

佈局,就是要找到元素間的幾何位置關係。主線程會遍歷DOM元素及其計算樣式,而後構造一棵佈局樹,這棵樹的每一個節點將帶有座標和大小信息。

佈局樹與DOM樹的結構相似,但只包含頁面中可見元素的信息。若是元素被應用了display: none,則佈局樹中不會包含它(visibility: hidden的元素會包含在內)。相似地,經過僞類p::before{content: 'Hi!'}添加的內容會包含在佈局樹中,但DOM樹中卻沒有。

爲了構建佈局樹,瀏覽器大致上完成了下面這些工做:

  • 遍歷DOM樹中的全部可見節點,並把這些節點加到佈局中;
  • 而不可見的節點會被佈局樹忽略掉,如head標籤下面的所有內容,再好比body.p.span這個元素,由於它的屬性包含 dispaly:none,因此這個元素也沒有被包進佈局樹。

肯定頁面的佈局要考慮不少不少因素,並不簡單。好比,字體大小、文本換行都會影響段落的形狀,進而影響後續段落的佈局。CSS可以讓元素浮動到一邊、隱藏溢出邊界的內容、改變文本顯示方向。可想而知,佈局階段的任務是很是艱鉅的。Chrome有一個工程師團隊專司佈局,感興起的話,能夠看看他們這個分享:BlinkOn 8: Block Layout Deep Dive(在YouTube上)。

更新了元素的幾何屬性(重排)

從上圖能夠看出,若是你經過JavaScript或者CSS修改元素的幾何位置屬性,例如改變元素的寬度、高度等,那麼瀏覽器會觸發從新佈局,解析以後的一系列子階段,這個過程就叫重排。無疑,重排鬚要更新完整的渲染流水線,因此開銷也是最大的

分層(Layer)

由於頁面中有不少複雜的效果,如一些複雜的3D變換、頁面滾動,或者使用z-indexing作z軸排序等,爲了更加方便地實現這些效果,渲染引擎還須要爲特定的節點生成專用的圖層,並生成一棵對應的圖層樹(LayerTree)。

要想直觀地理解什麼是圖層,你能夠打開Chrome的「開發者工具」,選擇「Layers」標籤(開發者工具 -> More tools ->Layers),就能夠可視化頁面的分層狀況。

一般狀況下,並非佈局樹的每一個節點都包含一個圖層,若是一個節點沒有對應的層,那麼這個節點就從屬於父節點的圖層。

那麼須要知足什麼條件,渲染引擎纔會爲特定的節點建立新的層呢?

第一點,擁有層疊上下文屬性的元素會被提高爲單獨的一層。

從圖中能夠看出,明肯定位屬性的元素、定義透明屬性的元素、使用CSS濾鏡的元素等,都擁有層疊上下文屬性。

第二點,須要剪裁(clip)的地方也會被建立爲圖層。

<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p>因此元素有了層疊上下文的屬性或者須要被剪裁,那麼就會被提高成爲單獨一層,你能夠參看下圖:</p>
        <p>從上圖咱們能夠看到,document層上有A和B層,而B層之上又有兩個圖層。這些圖層組織在一塊兒也是一顆樹狀結構。</p>
        <p>圖層樹是基於佈局樹來建立的,爲了找出哪些元素須要在哪些層中,渲染引擎會遍歷佈局樹來建立層樹(Update LayerTree)。</p> 
    </div>
</body>

在這裏咱們把div的大小限定爲200 200像素,而div裏面的文字內容比較多,文字所顯示的區域確定會超出200 200的面積,這時候就產生了剪裁。

出現這種裁剪狀況的時候,渲染引擎會爲文字部分單首創建一個層,若是出現滾動條,滾動條也會被提高爲單獨的層。

若是頁面某些部分應該獨立一層(如滑入的菜單)但卻沒有,那你能夠在CSS中給它加上will-change屬性來提醒瀏覽器。

分層並非越多越好,合成過多的層有可能還不如每幀都對頁面中的一小部分執行一次柵格化更快。關於這裏邊的權衡,能夠參考:堅持僅合成器的屬性和管理層計數

圖層繪製(Paint)

在完成圖層樹的構建以後,渲染引擎會對圖層樹中的每一個圖層進行繪製,那麼接下來咱們看看渲染引擎是怎麼實現圖層繪製的?

渲染引擎實現圖層的繪製,會把一個圖層的繪製拆分紅不少小的繪製指令,而後再把這些指令按照順序組成一個待繪製列表,以下圖所示:

從圖中能夠看出,繪製列表中的指令其實很是簡單,就是讓其執行一個簡單的繪製操做,好比繪製粉色矩形或者黑色的線等。

而繪製一個元素一般須要好幾條繪製指令,由於每一個元素的背景、前景、邊框都須要單獨的指令去繪製。因此在圖層繪製階段,輸出的內容就是這些待繪製列表

更新元素的繪製屬性(重繪)

從圖中能夠看出,若是修改了元素的背景顏色,那麼佈局階段將不會被執行,由於並無引發幾何位置的變換,因此就直接進入了繪製階段,而後執行以後的一系列子階段,這個過程就叫重繪。相較於重排操做,重繪省去了佈局和分層階段,因此執行效率會比重排操做要高一些

柵格化(raster)

繪製列表只是用來記錄繪製順序和繪製指令的列表,而實際上繪製操做是由渲染引擎中的合成線程來完成的。

當圖層的繪製列表準備好以後,主線程會把該繪製列表提交(commit)給合成線程

  • 一般一個頁面可能很大,可是用戶只能看到其中的一部分,咱們把用戶能夠看到的這個部分叫作視口(viewport)。
  • 有些狀況下,圖層很大,可是經過視口,用戶只能看到頁面的很小一部分。因此這種狀況下,要繪製出全部圖層內容的話,沒有必要。

    基於這個緣由,合成線程會將圖層劃分爲圖塊(tile),這些圖塊的大小一般都是256 x 256 或者 512 x 512。

合成器線程會安排柵格化線程優先轉換視口(及附近)的圖塊。而構成一層的圖塊也會轉換爲不一樣分辨率的版本,以便在用戶縮放時使用。

所謂柵格化,是指將圖塊轉換爲位圖。而圖塊是柵格化執行的最小單位。渲染進程維護了一個柵格化的線程池,全部的圖塊柵格化都是在線程池內執行的。

一般,柵格化過程都會使用GPU來加速生成,使用GPU生成位圖的過程叫快速柵格化,或者GPU柵格化,生成的位圖被保存在GPU內存中。

相信你還記得,GPU操做是運行在GPU進程中,若是柵格化操做使用了GPU,那麼最終生成位圖的操做是在GPU中完成的,這就涉及到了跨進程操做。

合成

什麼是合成?合成(composite)是將頁面不一樣部分先分層並分別柵格化,而後再經過獨立的合成器線程合成頁面。

這樣當用戶滾動頁面時,由於層都已經柵格化,因此瀏覽器惟一要作的就是合成一個新的幀。而動畫也能夠用一樣的方式實現:先移動層,再合成幀。

全部小片都柵格化之後,合成器線程會收集叫作「繪製方塊」(draw quad)的圖塊信息,以建立合成器幀。

  • 繪製方塊:包含小片的內存地址、頁面位置等合成頁面相關的信息。
  • 合成器幀:由從多繪製方塊拼成的頁面中的一幀。

建立好的合成器幀會經過IPC提交給瀏覽器進程。瀏覽器進程裏面有一個叫viz的組件,用來接收合成線程發過來的DrawQuad命令,而後根據DrawQuad命令,將其頁面內容繪製到內存中,最後再將內存顯示在屏幕上。

與此同時,爲更新瀏覽器界面,UI線程可能還會添加另外一個合成器幀;或者由於有擴展,其餘渲染器進程也可能添加額外的合成器幀。

全部這些合成器幀都會發送給GPU,以便最終顯示在屏幕上。若是發生滾動事件,合成器線程會再建立新的合成器幀併發送給GPU。

使用合成的好處是不用牽扯主線程。合成器線程不用等待樣式計算或JavaScript執行。

這也是爲何「只需合成的動畫」(【中】High Performance Animations)被認爲性能最佳的緣由。由於若是佈局和繪製須要再次計算,那還得用到主線程。

用一張圖來展現:

渲染流水線大總結

好了,咱們如今已經分析完了整個渲染流程,從HTML到DOM、樣式計算、佈局、圖層、繪製、光柵化、合成和顯示。下面我用一張圖來總結下這整個渲染流程:

  1. 渲染進程將HTML內容轉換爲可以讀懂的DOM樹結構。
  2. 渲染引擎將CSS樣式錶轉化爲瀏覽器能夠理解的styleSheets,計算出DOM節點的樣式。
  3. 建立佈局樹,並計算元素的佈局信息。
  4. 對佈局樹進行分層,並生成分層樹。
  5. 爲每一個圖層生成繪製列表,並將其提交到合成線程。
  6. 合成線程將圖層分紅圖塊,並在光柵化線程池中將圖塊轉換成位圖。
  7. 合成線程發送繪製圖塊命令DrawQuad給瀏覽器進程。
  8. 瀏覽器進程根據DrawQuad消息生成頁面,並顯示到顯示器上。

參考:

深刻理解現代瀏覽器

瀏覽器工做原理與實踐

[[譯] 現代瀏覽器內部揭祕(第一部分)](https://juejin.cn/post/684490...

相關文章
相關標籤/搜索