瀏覽器渲染流程總結

瀏覽器中的進程介紹

進程和線程的概念

進程:一個進程就是一個程序的運行實例。啓動一個程序的時候,操做系統會爲該程序建立一塊內存,用來存放代碼、運行中的數據和一個執行任務的主線程。html

線程:線程不能單獨存在,須要由進程來啓動和管理。算法

進程和線程的特色:canvas

  1. 進程中任一線程執行出錯都會致使整個進程崩潰。
  2. 線程之間共享進程中的數據。
  3. 當一個進程關閉以後,操做系統會回收進程所佔用的內存,即便其中有線程因執行不當致使內存泄露,進程退出時,這些內存也會被正確回收。
  4. 進程之間的內容相互隔離,使用進程間通訊(IPC)機制。
瀏覽器的發展歷程
單進程瀏覽器時代

瀏覽器全部功能模塊都運行在同一個進程裏,這些模塊包括網絡、插件、js運行環境、渲染引擎和頁面。帶來的問題:瀏覽器

  • 不穩定:一個插件的意外崩潰會引發整個瀏覽器的崩潰。
  • 不流暢:全部頁面的渲染模塊、js執行環境及插件都運行在同一個網頁線程中,同一時刻只有一個模塊能夠執行。
  • 不安全:插件可使用C/C++等代碼編寫,經過插件能夠獲取到操做系統的任意資源,頁面運行插件意味着這個插件能夠徹底操做你的電腦。頁面腳本也能夠經過瀏覽器漏洞獲取系統權限引起安全問題。
多進程瀏覽器時代

早期多進程架構中,頁面運行在單獨的渲染進程中,插件也是單獨的插件進程,進程之間經過IPC通訊。這就解決了單進程帶來的問題:進程間相互隔離,不會互相影響,也不會互相阻塞,並且插件進程和渲染進程使用了安全沙箱,程序在沙箱中運行,不能在硬盤上讀寫數據。緩存

目前最新的Chrome進程架構圖:安全

包括:1個瀏覽器(Browser)主進程、1個GPU進程、1個網絡(NetWork)進程、多個渲染進程、多個插件進程。因爲渲染進程和插件進程須要運行用戶代碼,所以安全起見,被隔離在沙箱中。各進程功能:性能優化

  • 瀏覽器進程。主要負責界面顯示、用戶交互、子進程管理,同時提供存儲等功能。
  • 渲染進程:核心任務是將HTML、CSS和JavaScript轉換爲用戶能夠與之交互的網頁,排版引擎 Blink 和 JavaScript 引擎 V8 都是運行在該進程中,默認狀況下,Chrome 會爲每一個Tab標籤頁建立一個渲染進程。
  • GPU 進程:最開始爲了實現 3D CSS 的效果,隨後網頁、Chrome 的 UI 界面都選擇採用 GPU 來繪製。
  • 網絡進程:主要負責頁面的網絡資源加載。
  • 插件進程:主要是負責插件的運行,因插件易崩潰,因此須要經過插件進程來隔離,以保證插件進程崩潰不會對瀏覽器和頁面形成影響。

多進程模型的缺點:服務器

  • 更高的資源佔用。由於每一個進程都會包含公共基礎結構的副本(如 JavaScript 運行環境),這就意味着瀏覽器會消耗更多的內存資源。
  • 更復雜的體系架構。瀏覽器各模塊之間耦合性高、擴展性差等問題,會致使如今的架構已經很難適應新的需求了。
新頁面的渲染進程策略

確切地說,有鏈接關係的同一站點(協議相同、根域名相同)會共用一個渲染進程。 如下方法可使兩個標籤頁進行鏈接:markdown

  1. 在A標籤頁中使用a標籤如<a href="./b.html" target="_blabk"> B </a>打開B標籤頁,此時B標籤頁中能夠經過window.opener訪問A的window。
  2. 經過window.open()方法打開的標籤頁。

這種有鏈接關係的標籤頁叫作瀏覽上下文組,Chrome瀏覽器會將瀏覽上下文組中屬於同一站點的標籤頁分配到同一個渲染進程中。但也有例外的狀況:網絡

  1. a標籤的ref屬性如<a target="_blank" ref="noopener noreferrer">,其中noopener會將新開標籤頁的opener值設爲null,noreferrer表示新開的標籤頁不要有引用關係。這就使得新開標籤頁和當前標籤頁不屬於同一個瀏覽上下文組了。
  2. 若是當前標籤頁中的iframe和標籤頁屬於不一樣站點,iframe也會運行在單獨的渲染進程中。

導航流程

從用戶發出url請求到頁面開始解析的過程叫作導航。

從輸入url到頁面展現的流程:

  1. 用戶輸入:用戶在地址欄輸入內容後,地址欄會判斷是搜索內容仍是請求的url,若是是搜索內容則使用默認搜索引擎來合成帶搜索關鍵字的url;若是是url則會根據規則加上協議合成完整的url。瀏覽器開始加載,標籤頁的圖標進入加載狀態,此時頁面仍是以前的頁面內容。
  2. 請求資源:瀏覽器進程經過IPC把url請求發送給網絡進程,網絡進程來發起真正的請求流程。
    • 查找緩存:若有,則直接返回資源給瀏覽器進程,如無則進入網絡請求流程。
    • 創建鏈接:進行DNS解析獲取IP地址,創建TCP鏈接和TLS鏈接(若是使用HTTPS的話)。
    • 發起請求:鏈接創建後,構建請求行、請求頭等信息,並將相關Cookie等數據附加到請求頭中,而後向服務器發送構建的請求信息。
    • 接收響應:網絡進程接收到服務器發來的響應後開始解析響應內容。若是發現狀態碼是301或302,則從響應頭的Location字段讀取重定向地址,再發起新的請求。
    • 處理響應數據類型:根據Content-Type判斷響應體的數據類型,若是是application/octet-stream則表示數據是字節流類型,會按下載類型處理,該請求會被提交給瀏覽器的下載管理器,導航流程就此結束。若是是text/html類型,則會開始準備渲染進程。
  3. 準備渲染進程:瀏覽器進程從網絡進程中的得知是html類型後,會爲該請求選擇或建立一個渲染進程。
  4. 提交文檔:瀏覽器進程向渲染進程發送「提交文檔」的消息,渲染進程收到後,和網絡進程創建傳輸數據的「管道」。等響應體數據傳輸完成後,渲染進程會返回「確認提交」的消息給瀏覽器進程,瀏覽器進程更新界面狀態:安全狀態、地址欄的url、前進後退的歷史狀態,更新頁面。

確認提交後,導航流程結束,開始進入渲染流程。

渲染流程

渲染進程會先建立一個空白頁面,叫作解析白屏。以後開始頁面解析和子資源加載。

一 構建DOM樹(Parse HTML)——產出DOM樹

因爲瀏覽器沒法理解HTML,所以須要將HTML解析爲瀏覽器能理解的結構——DOM樹。DOM是保存在內存中的樹結構,DOM是生成頁面的基礎數據結構,給JavaScript腳本提供一套查詢和改變文檔結構、樣式、內容的接口,也是一道安全防禦線,一些不安全的內容在DOM解析階段就被排除了。

渲染引擎內部有個HTML解析器的模塊,負責將HTML字節流轉換爲DOM結構。解析過程是漸進的,網絡進程加載了多少數據,HTML解析器就解析多少數據。渲染進程從以前和網絡進程創建的數據管道的一端動態接收字節流,並將其解析爲DOM。

解析過程

  1. 經過分詞器將字節流轉換爲Token,分爲Tag Token和文本Token。

  2. 將Token解析爲DOM節點,而後將節點添加到DOM樹中。HTML解析器維護了一個Token棧結構,若是是StartTag Token則入棧,並建立一個DOM節點添加到DOM樹中(HTML解析器最初會默認建立一個根爲document的空DOM結構);若是是文本Token則生成文本節點添加到DOM樹中;若是是EndTag Token則檢查棧頂是不是相同標籤的StartTag Token,是則出棧。

JavaScript和CSS對DOM構建的影響

解析過程當中遇到了script標籤時,HTML解析器暫停工做,JavaScript引擎介入,執行腳本,此時只能訪問位於script之上已經構建了的DOM。訪問後面的元素會返回null,執行操做會報錯。腳本執行完,HTML解析器恢復解析。

若是不是內嵌腳本,而是經過src加載的腳本(有src屬性的腳本會忽略標籤內的代碼),會等待下載完成後執行,期間HTML解析器一直是暫停的狀態。不過好在Chrome瀏覽器的預解析操做會在渲染引擎收到字節流以後開啓一個預解析線程分析HTML文件中包含的JavaScript、CSS等相關文件而後提早下載這些文件。

在有src屬性加載腳本的script標籤中添加async屬性代表這個腳本是異步的,在後臺加載,HTML解析器繼續工做,等腳本加載完成後當即打斷HTML解析器(若是還沒解析完的話)開始執行腳本。 添加defer屬性也會異步加載,可是會等到HTML解析完畢,在DOMContentLoaded事件以前執行。這兩個屬性對無src屬性的腳本不會生效。

對於CSS,因爲不直接參與DOM構建,原本是不會阻塞DOM樹的構建的。可是若是頁面有同步執行的腳本時,由於執行前是不知道腳本有沒有操做CSSOM的,所以渲染引擎爲了不腳本執行出錯的可能性,直接假定腳本會依賴CSSOM。因而下載CSS文件並解析成CSSOM,再執行腳本。期間HTML解析器一直是暫停的狀態,直到腳本執行完畢才繼續工做。

頁面無腳本時CSS不會阻塞DOM樹的構建:

頁面有同步腳本時CSS會阻塞DOM樹的構建:

頁面有異步腳本時,會繼續構建DOM樹,腳本執行前須要等待CSS。是否繼續渲染則須要看CSS是否就緒,若是CSS還在加載中則須要等待。

  • 若是是defer腳本,CSS加載完就開始渲染,只是DOMContentLoaded和load事件會延遲觸發;

  • 若是是async腳本,DOMContentLoaded正常觸發,CSS加載完就開始渲染,load事件會等待腳本執行完再觸發:

總之CSS阻塞DOM樹的構建與否取決於腳本執行的時機。

二 樣式計算(Recalculate Style)——產出ComputedStyle

樣式計算是爲了計算出DOM節點中每一個元素的具體樣式,這個階段大致能夠分爲三個步驟:

  1. 構建CSSOM。CSS的來源有三種:經過link標籤引用的外部CSS、style標籤、元素的style屬性。因爲瀏覽器也沒法直接理解純文本的CSS,所以渲染引擎須要先將其轉爲瀏覽器能夠理解的結構——CSSOM(能夠經過document.styleSheets訪問)。最終會把全部不一樣來源的樣式都包含進styleSheets中。CSSOM的做用一個是爲JavaScript提供操做樣式表的接口,一個是爲佈局樹的合成提供基礎的樣式信息。
  2. 標準化樣式屬性值:將例如2em、blue、bold之類的數值轉換爲渲染引擎容易理解的、標準化的計算值。最終解析成相似32px、rgb(255,0,0)、700這樣的標準值。
  3. 計算DOM樹中每一個節點的具體樣式:須要使用兩個規則:
    • 繼承:每一個DOM節點都會包含父節點中能夠繼承的屬性,例如body的font-size。
    • 層疊:定義了合併多個來源的屬性值的算法,計算結果保存在ComputedStyle的結構內。
三 佈局(Layout)——產出佈局樹

此階段用來計算DOM樹中可見元素的幾何位置。有兩個步驟:

  1. 建立佈局樹:遍歷DOM樹,用全部可見節點構建一顆佈局樹,不可見的節點如head或display:none的元素都會被過濾掉。這個過程的樣式查詢會使用到ComputedStyle。

  1. 佈局計算:計算佈局樹中節點的座標位置,把佈局計算的結果從新寫回佈局樹中。所以佈局樹既是輸入內容,也是輸出內容,這也是佈局階段一個不合理的地方,Chrome團隊正在重構佈局代碼,下一代佈局系統叫LayoutNG,將會嘗試分離輸入和輸出,簡化佈局算法。
四 分層(Update Layer Tree)——產出圖層樹

因爲頁面中可能包含不少複雜的效果如3D變換、頁面滾動、z-index的z軸排序等,爲了不每次改動都要引起整個頁面重排或重繪,會將頁面分紅多個圖層,並生成一顆對應的圖層樹(LayerTree),層樹中的每一個節點都對應一個圖層,這些圖層按照必定順序疊加在一塊兒(層合成composite)構成了最終的頁面圖像。

一般,並不是佈局樹的每一個節點都佔用一個圖層,沒有圖層的節點將會從屬於父節點的圖層,最終每一個節點都會直接或間接包含在一個圖層中。處於相同的z軸座標空間時,就會造成一個圖層(或渲染層RenderLayers)。

渲染引擎會爲一些特定的節點建立單獨的圖層:

  1. 知足如下特殊條件:
  • 3D transforms:translate3d、translateZ 等
  • video、canvas、iframe 等元素
  • 經過 Element.animate() 實現的 opacity 動畫轉換
  • 經過 СSS 動畫實現的 opacity 動畫轉換
  • position: fixed
  • 具備 will-change 屬性
  • 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition
  1. 能夠滾動的overflow元素:元素的內容超出容器時,若是設置了overflow使得內容能夠滾動,則分別會爲容器、滾動的所有內容、滾動條建立各自的圖層。這樣滾動時,各部分就不會由於重排/重繪而相互影響。

隱式建立圖層(合成層)

若是一個元素覆蓋在一個擁有獨立圖層的元素上,而且這個元素的z-index值更大,那這個元素也會被提高到單獨的圖層中。

層爆炸和層壓縮

像上面那種隱式建立圖層的狀況,若是不加註意,極端場景下可能會建立大量獨立圖層,也就是層爆炸,會佔用GPU和大量的內存資源,嚴重損耗頁面性能。瀏覽器也有相應的對策,瀏覽器的層壓縮機制,會將隱式建立的多個圖層壓縮到一個圖層中。

z-index:3這個元素是一個獨立的圖層,其後z-index值更大的三個元素被壓縮進了一個圖層中:

可是不少特定狀況下,瀏覽器是沒法進行層壓縮的。例如若是頁面的某一部分使用transform和animation製做動畫效果,因爲可能產生動態交疊的狀況,隱式建立圖層在不須要交疊的狀況下也能發生,致使頁面中全部z-index高於它的節點都被提高到單獨的圖層中,使得頁面產生了大量的獨立圖層。消除的辦法就是增大z-index的值,或者合理優化頁面元素的結構。

五 圖層繪製(Paint)——產出圖層的待繪製列表

獲得圖層樹以後,渲染引擎會對圖層樹中的每一個圖層進行繪製,把一個圖層的繪製拆分紅不少小的繪製指令,再把這些指令按照順序組成一個待繪製列表:

圖層繪製階段輸出的內容就是這些待繪製列表。能夠在開發者工具中查看一個圖層的繪製列表,在區域2拖動進度條能夠重現列表的繪製過程:

六 柵格化(raster)——產出合成後的位圖

當圖層的繪製列表準備好以後,渲染進程中的主線程會把該繪製列表提交給合成線程。

一般頁面內容都比屏幕大得多,若是等待全部圖層都繪製完畢再進行合成的話,會產生一些沒必要要的開銷,也會讓合成圖片的時間變得更久。合成線程會將圖層劃分爲固定的圖塊(tile),大小一般是256x256或512x512。若是這個圖層很是大,會優先處理視口(屏幕上頁面的可見區域)附近的圖塊。

渲染進程維護了一個柵格化的線程池,全部圖塊的柵格化都是在這個線程池內執行。 柵格化就是按照繪製列表的指令生成圖片,每一個圖層都對應一張圖片,合成線程將這些圖片合成爲「一張」圖片。圖塊是刪格化執行的最小單位。

一般,柵格化過程都會使用GPU來加速生成,這個過程叫作快速柵格化或GPU柵格化,渲染進程把生成位圖的指令發送給GPU,生成的位圖保存在GPU內存中。

但即便只繪製優先級最高的圖塊,也要耗費很多時間,由於會涉及到一個很關鍵的因素——紋理上傳(存儲在共享內存中的位圖將做爲紋理上傳到GPU,最後由GPU將多個位圖進行合成),這是由於從計算機內存上傳到GPU內存的操做會比較慢。Chrome採起的策略是,在首次合成圖塊的時候使用一個低分辨率的圖片,分辨率減小一半,紋理就減小了四分之三。在首次顯示頁面內容的時候,將這個低分辨率的圖片顯示出來,而後合成器繼續繪製正常比例的網頁內容,繪製完成後再替換掉當前顯示的低分辨率內容。

因爲合成操做在合成線程上完成,不會影響主線程,這就是爲何常常主線程卡住了但CSS動畫依然能執行的緣由。而使用JavaScript實現動畫時,會牽涉到整個渲染流水線,繪製效率底下。涉及到一些可使用合成線程來處理CSS特效或動畫的狀況,可使用will-change來提早告訴渲染引擎,讓它爲該元素準備獨立的圖層,而且開啓GPU加速。

例如will-change:transform,opacity;告訴渲染引擎將會對該元素作一些特效變換,渲染引擎會爲該元素單獨分配一個圖層,當這些變換髮生時,渲染引擎會經過合成線程直接去處理變換,因爲沒有涉及到主線程,大大提高了渲染效率。因此CSS動畫比JavaScript動畫高效。

但這樣它佔用的內存也會增長,由於從層樹開始,後面每一個階段都會多一個層結構,這些都須要額外的內存。

七 顯示

一旦全部圖塊都被柵格化,合成線程就會生成一個繪製圖塊的指令——「DrawQuard」,而後將該指令提交給瀏覽器進程。瀏覽器進程中的viz組件接收到指令後,將頁面內容繪製到內存中,最後再將內存中的頁面顯示在屏幕上。

每一個顯示器都有固定的刷新頻率,一般是60Hz,即每秒更新60張圖片,更新的圖片都來自於顯卡的前緩衝區。顯示器每秒固定讀取60次前緩衝區中的圖像來顯示。顯卡中的GPU合成新的圖像,保存到後緩衝區。而後系統將顯卡的先後緩衝區互換,來讓顯示器讀取最新的圖像。一般,顯卡的更新頻率和顯示器的刷新頻率是一致的,但有時在一些複雜場景中,顯卡處理一張圖片的速度變慢,就形成了視覺上的卡頓。

頁面上的動畫效果例如滾動,渲染引擎經過渲染流水線生成新的圖片發送到顯卡的後緩衝區,要想實現流暢的動畫效果,渲染引擎須要每秒更新60張圖片到顯卡的後緩衝區。每一副圖片稱爲一幀,每秒更新了多少幀稱爲幀率,若是滾動過程當中1秒更新了60幀,幀率就是60Hz(或60FPS)。若是一次動畫中,渲染引擎生成某些幀的時間太久,用戶就會感受到卡頓。

渲染流程結束後,渲染進程會發送一個消息給瀏覽器進程,瀏覽器進程中止標籤圖標上的加載動畫。

總結

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

幾個和性能優化相關的概念:

重排

若是修改了元素的幾何位置屬性,如改變寬高,會引起從新佈局及以後一系列流程,這個過程叫作重排。

調用如下DOM API,爲了保證結果的準確性,瀏覽器會觸發重排以獲取最新的信息:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight(元素包含邊框的寬/高)
  • scrollTop、scrollLeft、scrollWidth、scrollHeight(元素包含內邊距的包含不可見的滾動內容的寬/高)
  • clientTop、clientLeft、clientWidth、clientHeight(元素包含內邊距的寬/高)
  • window.getComputedStyle(elem)、IE裏的currentStyle
  • getBoundingClientRect()
重繪

不改變幾何位置信息,只改變元素的如背景顏色等信息,引起從新繪製及以後一系列流程,這個過程叫作重繪。

合成

若是既不要佈局也不要繪製的屬性,渲染引擎會跳過佈局和繪製,只執行後續的合成操做,這個過程叫作合成。例如使用CSS的transform來實現動畫效果,能夠避開重排和重繪階段,在非主線程上執行合成動畫的操做,無需佔用主線程的資源,大大提升了效率。

所以在執行效率方面:合成 > 重繪 > 重排。在性能優化方面能夠依據這個原則來調整方案。

References

  1. 極客時間:《瀏覽器渲染原理與實踐》
  2. 瀏覽器合成與渲染層優化:mp.weixin.qq.com/s/knmQ1XRwt…
相關文章
相關標籤/搜索