- 原文地址:Inside look at modern web browser (part 3)
- 原文做者:Mariko Kosaka
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:ssshooter
- 校對者:ThomasWhyne, CoolRice
這是關於瀏覽器工做原理博客系列四部分中的第三部分。以前,咱們介紹了多進程架構和導航流。在這篇文章中,咱們將一探渲染進程的內部機制。javascript
渲染進程涉及 Web 性能的許多方面。因爲渲染進程的流程太複雜,所以本文只進行概述。若是你想深刻了解,能夠在 the Performance section of Web Fundamentals 找到相關資源。css
渲染進程負責標籤頁內發生的全部事情。在渲染進程中,主線程處理服務器發送到用戶的大部分代碼。若是你使用 web worker 或 service worker,部分 JavaScript 將由工做線程處理。合成和光柵線程也在渲染進程內運行,以高效,流暢地呈現頁面。html
渲染進程的核心工做是將 HTML、CSS 和 JavaScript 轉換爲用戶能夠與之交互的網頁。前端
圖 1:渲染進程內部包含主線程、工做線程、合成線程和光柵線程html5
當渲染進程收到導航的提交消息並開始接收 HTML 數據時,主線程開始解析文本字符串(HTML)並將其轉換爲文檔對象模型(DOM)。java
DOM 是一個頁面在瀏覽器內部表現,也是 Web 開發人員能夠經過 JavaScript 與之交互的數據結構和 API。android
將 HTML 到 DOM 的解析由 HTML Standard 規定。你可能已經注意到,將 HTML 提供給瀏覽器這一過程從不會引起錯誤。像 Hi! <b>I'm <i>Chrome</b>!</i>
這樣的錯誤標記,會被理解爲 Hi! <b>I'm <i>Chrome</i></b><i>!</i>
,這是由於 HTML 規範會優雅地處理這些錯誤。若是你好奇這是如何作到的,能夠閱讀 An introduction to error handling and strange cases in the parser 的 HTML 規範部分。ios
網站一般使用圖像、CSS 和 JavaScript 等外部資源,這些文件須要從網絡或緩存加載。在解析構建 DOM 時,主線程會按處理順序逐個請求它們,但爲了加快速度,「預加載掃描器(preload scanner)」會同時運行。若是 HTML 文檔中有 <img>
或 <link>
之類的內容,則預加載掃描器會查看由 HTML 解析器生成的標記,並在瀏覽器進程中向網絡線程發送請求。git
圖 2:主線程解析 HTML 並構建 DOM 樹github
當 HTML 解析器遇到 <script>
標記時,會暫停解析 HTML 文檔,開始加載、解析並執行 JavaScript 代碼。爲何?由於JavaScript 可使用諸如 document.write()
的方法來改寫文檔,這會改變整個 DOM 結構(HTML 規範裏的 overview of the parsing model 中有一張不錯的圖片)。這就是 HTML 解析器必須等待 JavaScript 運行後再繼續解析 HTML 文檔緣由。若是你對 JavaScript 執行中發生的事情感到好奇,能夠看看 V8 團隊就此發表的演講和博客文章。
Web 開發者能夠經過多種方式向瀏覽器發送提示,以便很好地加載資源。若是你的 JavaScript 不使用 document.write()
,你能夠在 <script>
標籤添加 async
或 defer
屬性,這樣瀏覽器會異步加載運行 JavaScript 代碼,而不阻塞解析。若是合適,你也可使用 JavaScript 模塊。可使用 <link rel="preload">
告知瀏覽器當前導航確定須要該資源,而且你但願儘快下載。有關詳細信息請參閱 Resource Prioritization – Getting the Browser to Help You。
只擁有 DOM 不足以肯定頁面的外觀,由於咱們會在 CSS 中設置頁面元素的樣式。主線程解析 CSS 並肯定每一個 DOM 節點計算後的樣式。這是有關基於 CSS 選擇器對每一個元素應用何種樣式的信息,這能夠在 DevTools 的 computed
部分中看到。
圖 3:主線程解析 CSS 以添加計算後樣式
即便你不提供任何 CSS,每一個 DOM 節點都具備計算樣式。像 <h1>
標籤看起來比 <h2>
標籤大,每一個元素都有 margin,這是由於瀏覽器具備默認樣式表。若是你想知道更多 Chrome 的默認 CSS,能夠在這裏看到源代碼。
如今,渲染進程知道每一個節點的樣式和文檔的結構,但這不足以渲染頁面。想象一下,你正試圖經過手機向朋友描述一幅畫:「這裏有一個大紅圈和一個小藍方塊」,這並不能讓你的朋友知道這幅畫究竟長什麼樣。
圖 4:一我的站在一幅畫前,電話線與另外一我的相連
佈局是計算元素幾何形狀的過程。主線程遍歷 DOM,計算樣式並建立佈局樹,其中包含 x y 座標和邊界框大小等信息。佈局樹可能與 DOM 樹結構相似,但它僅包含頁面上可見內容相關的信息。若是一個元素應用了 display:none
,那麼該元素不是佈局樹的一部分(但 visibility:hidden
的元素在佈局樹中)。相似地,若是應用瞭如 p::before{content:"Hi!"}
的僞類,則即便它不在 DOM 中,也包含於佈局樹中。
圖 5:主線程遍歷計算樣式後的 DOM 樹,以今生成佈局樹
圖 6:因爲換行而移動的盒子佈局
肯定頁面佈局是一項頗有挑戰性的任務。即便是從上到下的塊流這樣最簡單的頁面佈局,也必須考慮字體的大小以及換行位置,這些因素會影響段落的大小和形狀,進而影響下一個段落的位置。
CSS 可使元素浮動到一側、隱藏溢出的元素、更改書寫方向。你能夠想象這一階段的任務之艱鉅。Chrome 瀏覽器有整個工程師團隊負責佈局。BlinkOn 會議的一些訪談記錄了他們工做的細節,有興趣能夠了解一下,挺有趣的。
圖 7:一我的拿着筆站在畫布前,思考着她應該先畫圓形仍是先畫方形
擁有 DOM、樣式和佈局仍然不足以渲染頁面。假設你正在嘗試重現一幅畫。你知道元素的大小、形狀和位置,但你仍須要判斷繪製它們的順序。
例如,能夠爲某些元素設置 z-index
,此時按 HTML 中編寫的元素的順序繪製會致使錯誤的渲染。
圖 8:由於沒有考慮 z-index,頁面元素按 HTML 標記的順序出現,致使錯誤的渲染圖像
在繪製步驟中,主線程遍歷佈局樹建立繪製記錄。繪製記錄是繪圖過程的記錄,就像是「背景優先,而後是文本,而後是矩形」。若是你使用過 JavaScript 繪製了 <canvas>
元素,那麼這個過程對你來講可能很熟悉。
圖 9:主線程遍歷佈局樹並生成繪製記錄
圖 10:DOM + Style、佈局和繪製樹的生成順序
渲染管道中最重要的事情是:每一個步驟中,前一個操做的結果用於後一個操做建立新數據。例如,若是佈局樹中的某些內容發生改變,須要爲文檔的受影響部分從新生成「繪製」指令。
若是要爲元素設置動畫,則瀏覽器必須在每一個幀之間運行這些操做。大多數顯示器每秒刷新屏幕 60 次(60 fps),當屏幕每幀都在變化,人眼會以爲動畫很流暢。可是,若是動畫丟失了中間一些幀,頁面看起來就會卡頓(janky)。
圖 11:時間軸上的動畫幀
即便渲染操做能跟上屏幕刷新,這些計算也會在主線程上運行,這意味着當你的應用程序運行 JavaScript 時動畫可能會被阻塞。
圖 12:時間軸上的動畫幀,但 JavaScript 阻塞了一幀
你能夠將 JavaScript 操做劃分爲小塊,並使用 requestAnimationFrame()
在每一個幀上運行。有關此主題的更多信息,請參閱 Optimize JavaScript Execution。你也能夠在 Web Worker 中運行 JavaScript 以免阻塞主線程。
圖 13:時間軸上較小的 JavaScript 塊與動畫幀一塊兒運行
圖 14:簡單光柵處理示意動畫
如今瀏覽器知道文檔的結構、每一個元素的樣式、頁面的幾何形狀和繪製順序,它是如何繪製頁面的?把這些信息轉換爲屏幕上的像素,咱們稱爲光柵化。
處理這種狀況的一種簡單的方法是,先在光柵化視窗內的畫面,若是用戶滾動頁面,則移動光柵框,並光柵化填充缺乏的部分。這就是 Chrome 首次發佈時處理光柵化的方式。可是,現代瀏覽器會運行一個更復雜的過程,咱們稱爲合成。
圖 15:合成處理示意動畫
合成是一種將頁面的各個部分分層,分別光柵化,並在稱爲合成線程的單獨線程中合成爲頁面的技術。若是發生滾動,因爲圖層已經光柵化,所以它所要作的只是合成一個新幀。動畫也能夠以相同的方式(移動圖層和合成新幀)實現。
你能夠在 DevTools 使用 Layers 面板 看看你的網站如何被分層。
爲了分清哪些元素位於哪些圖層,主線程遍歷佈局樹建立圖層樹(此部分在 DevTools 性能面板中稱爲「Update Layer Tree」)。若是頁面的某些部分應該是單獨圖層(如滑入式側面菜單)但沒拆分出來,你可使用 CSS 中的 will-change
屬性來提示瀏覽器。
圖 16:主線程遍歷佈局樹生成圖層樹
你可能想要爲每一個元素都分層,可是合成大量的圖層可能會比每幀都光柵化頁面的刷新方式更慢,所以測量應用程序的渲染性能相當重要。有關這個主題的更多信息,請參閱 Stick to Compositor-Only Properties and Manage Layer Count。
一旦建立了圖層樹並肯定了繪製順序,主線程就會將該信息提交給合成線程。接着,合成線程會光柵化每一個圖層。一個圖層可能會跟整個頁面同樣大,所以合成線程將它們分塊後發送到光柵線程。光柵線程光柵化每一個小塊後會將它們存儲在顯存中。
圖17:光柵線程建立分塊的位圖併發送到 GPU
合成線程會給不一樣的光柵線程設置優先級,以便視窗(或附近)內的畫面能夠先被光柵化。圖層還具備多個不一樣分辨率的塊,能夠處理放大操做等動做。
一旦塊被光柵化,合成線程會收集這些塊的信息(稱爲繪製四邊形)建立合成幀。
繪製四邊形
包含諸如圖塊在內存中的位置,以及合成時繪製圖塊在頁面中的位置等信息。
合成幀
一個繪製四邊形的集合,表明一個頁面的一幀。
接着,合成幀經過 IPC(進程間通信)提交給瀏覽器進程。此時,能夠從 UI 線程或其餘插件的渲染進程添加另外一個合成幀。這些合成器幀被髮送到 GPU 而後在屏幕上顯示。若是接收到滾動事件,合成線程會建立另外一個合成幀發送到 GPU。
圖 18:合成線程建立合成幀,將其發送到瀏覽器進程,再接着發送到 GPU
合成的好處是它能夠在不涉及主線程的狀況下完成。合成線程不須要等待樣式計算或 JavaScript 執行。這就是爲何僅合成動畫被認爲是流暢性能的最佳選擇。若是須要再次計算佈局或繪製,則必須涉及主線程。
在這篇文章中,咱們研究了渲染管道從解析到合成的整個過程,但願如今你能自主地去了解更多關於網站性能優化的信息。
在本系列的下一篇也是最後一篇文章中,咱們將更詳細地介紹合成線程,看看當用戶移動或點擊鼠標時會發生什麼。
你喜歡這篇文章嗎?若是你對以後的文章有任何問題或建議,我很樂意在下面的評論部分或推特 @kosamari 與你聯繫。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。