奔图内部扫描错误13_现代浏览器内部揭秘(第三部分)
這是關于瀏覽器工作原理博客系列四部分中的第三部分。之前,我們介紹了 多進程架構?和?導航流。在這篇文章中,我們將一探渲染進程的內部機制。
渲染進程的內部機制
渲染進程涉及 Web 性能的許多方面。由于渲染進程的流程太復雜,因此本文只進行概述。如果你想深入了解,可以在 the Performance section of Web Fundamentals[1] 找到相關資源。
渲染進程處理網站內容
渲染進程負責標簽頁內發生的所有事情。在渲染進程中,主線程處理服務器發送到用戶的大部分代碼。如果你使用 web worker 或 service worker,部分 JavaScript 將由工作線程處理。合成和光柵線程也在渲染進程內運行,以高效,流暢地呈現頁面。
渲染進程的核心工作是將 HTML、CSS 和 JavaScript 轉換為用戶可以與之交互的網頁。
Renderer process圖 1:渲染進程內部包含主線程、工作線程、合成線程和光柵線程
解析(Parsing)
DOM 的構建
當渲染進程收到導航的提交消息并開始接收 HTML 數據時,主線程開始解析文本字符串(HTML)并將其轉換為文檔對象模型(DOM)。
DOM 是一個頁面在瀏覽器內部表現,也是 Web 開發人員可以通過 JavaScript 與之交互的數據結構和 API。
將 HTML 到 DOM 的解析由 HTML Standard[2] 規定。你可能已經注意到,將 HTML 提供給瀏覽器這一過程從不會引發錯誤。像 Hi! I'm Chrome! 這樣的錯誤標記,會被理解為 Hi! I'm Chrome!,這是因為 HTML 規范會優雅地處理這些錯誤。如果你好奇這是如何做到的,可以閱讀 An introduction to error handling and strange cases in the parser[3] 的 HTML 規范部分。
子資源加載
網站通常使用圖像、CSS 和 JavaScript 等外部資源,這些文件需要從網絡或緩存加載。在解析構建 DOM 時,主線程會按處理順序逐個請求它們,但為了加快速度,“預加載掃描器(preload scanner)”會同時運行。如果 HTML 文檔中有 或 之類的內容,則預加載掃描器會查看由 HTML 解析器生成的標記,并在瀏覽器進程中向網絡線程發送請求。
DOM圖 2:主線程解析 HTML 并構建 DOM 樹
JavaScript 阻塞解析
當 HTML 解析器遇到 標記時,會暫停解析 HTML 文檔,開始加載、解析并執行 JavaScript 代碼。為什么?因為JavaScript 可以使用諸如 document.write() 的方法來改寫文檔,這會改變整個 DOM 結構(HTML 規范里的 overview of the parsing model[4] 中有一張不錯的圖片)。這就是 HTML 解析器必須等待 JavaScript 運行后再繼續解析 HTML 文檔原因。如果你對 JavaScript 執行中發生的事情感到好奇,可以看看 V8 團隊就此發表的演講和博客文章[5]。
提示瀏覽器如何加載資源
Web 開發者可以通過多種方式向瀏覽器發送提示,以便很好地加載資源。如果你的 JavaScript 不使用 document.write(),你可以在 標簽添加 `async`[6] 或 `defer`[7] 屬性,這樣瀏覽器會異步加載運行 JavaScript 代碼,而不阻塞解析。如果合適,你也可以使用 JavaScript 模塊[8]。可以使用 告知瀏覽器當前導航肯定需要該資源,并且你希望盡快下載。有關詳細信息請參閱 Resource Prioritization – Getting the Browser to Help You[9]。
樣式計算
只擁有 DOM 不足以確定頁面的外觀,因為我們會在 CSS 中設置頁面元素的樣式。主線程解析 CSS 并確定每個 DOM 節點計算后的樣式。這是有關基于 CSS 選擇器對每個元素應用何種樣式的信息,這可以在 DevTools 的 computed 部分中看到。
computed style圖 3:主線程解析 CSS 以添加計算后樣式
即使你不提供任何 CSS,每個 DOM 節點都具有計算樣式。像
標簽看起來比 標簽大,每個元素都有 margin,這是因為瀏覽器具有默認樣式表。如果你想知道更多 Chrome 的默認 CSS,可以在這里看到源代碼[10]。布局
現在,渲染進程知道每個節點的樣式和文檔的結構,但這不足以渲染頁面。想象一下,你正試圖通過手機向朋友描述一幅畫:“這里有一個大紅圈和一個小藍方塊”,這并不能讓你的朋友知道這幅畫究竟長什么樣。
game of human fax machine圖 4:一個人站在一幅畫前,電話線與另一個人相連
布局是計算元素幾何形狀的過程。主線程遍歷 DOM,計算樣式并創建布局樹,其中包含 x y 坐標和邊界框大小等信息。布局樹可能與 DOM 樹結構類似,但它僅包含頁面上可見內容相關的信息。如果一個元素應用了 display:none,那么該元素不是布局樹的一部分(但 visibility:hidden 的元素在布局樹中)。類似地,如果應用了如 p::before{content:"Hi!"} 的偽類,則即使它不在 DOM 中,也包含于布局樹中。
layout圖 5:主線程遍歷計算樣式后的 DOM 樹,以此生成布局樹
layout.gif圖 6:由于換行而移動的盒子布局
確定頁面布局是一項很有挑戰性的任務。即使是從上到下的塊流這樣最簡單的頁面布局,也必須考慮字體的大小以及換行位置,這些因素會影響段落的大小和形狀,進而影響下一個段落的位置。
CSS 可以使元素浮動到一側、隱藏溢出的元素、更改書寫方向。你可以想象這一階段的任務之艱巨。Chrome 瀏覽器由整個工程師團隊負責布局。BlinkOn 會議的一些訪談[11]記錄了他們工作的細節,有興趣可以了解一下,挺有趣的。
繪制
drawing game圖 7:一個人拿著筆站在畫布前,思考著她應該先畫圓形還是先畫方形
擁有 DOM、樣式和布局仍然不足以渲染頁面。假設你正在嘗試重現一幅畫。你知道元素的大小、形狀和位置,但你仍需要判斷繪制它們的順序。
例如,可以為某些元素設置 z-index,此時按 HTML 中編寫的元素的順序繪制會導致錯誤的渲染。
z-index fail圖 8:因為沒有考慮 z-index,頁面元素按 HTML 標記的順序出現,導致錯誤的渲染圖像
在繪制步驟中,主線程遍歷布局樹創建繪制記錄。繪制記錄是繪圖過程的記錄,就像是“背景優先,然后是文本,然后是矩形”。如果你使用過 JavaScript 繪制了 元素,那么這個過程對你來說可能很熟悉。
paint records圖 9:主線程遍歷布局樹并生成繪制記錄
更新渲染管道的成本很高
trees.gif圖 10:DOM + Style、布局和繪制樹的生成順序
渲染管道中最重要的事情是:每個步驟中,前一個操作的結果用于后一個操作創建新數據。例如,如果布局樹中的某些內容發生改變,需要為文檔的受影響部分重新生成“繪制”指令。
如果要為元素設置動畫,則瀏覽器必須在每個幀之間運行這些操作。大多數顯示器每秒刷新屏幕 60 次(60 fps),當屏幕每幀都在變化,人眼會覺得動畫很流暢。但是,如果動畫丟失了中間一些幀,頁面看起來就會卡頓(janky)。
jage jank by missing frames圖 11:時間軸上的動畫幀
即使渲染操作能跟上屏幕刷新,這些計算也會在主線程上運行,這意味著當你的應用程序運行 JavaScript 時動畫可能會被阻塞。
jage jank by JavaScript圖 12:時間軸上的動畫幀,但 JavaScript 阻塞了一幀
你可以將 JavaScript 操作劃分為小塊,并使用 requestAnimationFrame() 在每個幀上運行。有關此主題的更多信息,請參閱 Optimize JavaScript Execution[12]。你也可以在 Web Worker 中運行 JavaScript[13] 以避免阻塞主線程。
request animation frame圖 13:時間軸上較小的 JavaScript 塊與動畫幀一起運行
合成
如何繪制一個頁面?
naive_rastering.gif圖 14:簡單光柵處理示意動畫
現在瀏覽器知道文檔的結構、每個元素的樣式、頁面的幾何形狀和繪制順序,它是如何繪制頁面的?把這些信息轉換為屏幕上的像素,我們稱為光柵化。
處理這種情況的一種簡單的方法是,先在光柵化視窗內的畫面,如果用戶滾動頁面,則移動光柵框,并光柵化填充缺少的部分。這就是 Chrome 首次發布時處理光柵化的方式。但是,現代瀏覽器會運行一個更復雜的過程,我們稱為合成。
什么是合成
composit.gif圖 15:合成處理示意動畫
合成是一種將頁面的各個部分分層,分別光柵化,并在稱為合成線程的單獨線程中合成為頁面的技術。如果發生滾動,由于圖層已經光柵化,因此它所要做的只是合成一個新幀。動畫也可以以相同的方式(移動圖層和合成新幀)實現。
你可以在 DevTools 使用 Layers 面板[14] 看看你的網站如何被分層。
分層
為了分清哪些元素位于哪些圖層,主線程遍歷布局樹創建圖層樹(此部分在 DevTools 性能面板中稱為“Update Layer Tree”)。如果頁面的某些部分應該是單獨圖層(如滑入式側面菜單)但沒拆分出來,你可以使用 CSS 中的 will-change 屬性來提示瀏覽器。
layer tree圖 16:主線程遍歷布局樹生成圖層樹
你可能想要為每個元素都分層,但是合成大量的圖層可能會比每幀都光柵化頁面的刷新方式更慢,因此測量應用程序的渲染性能至關重要。有關這個主題的更多信息,請參閱 Stick to Compositor-Only Properties and Manage Layer Count[15]。
主線程的光柵化和合成
一旦創建了圖層樹并確定了繪制順序,主線程就會將該信息提交給合成線程。接著,合成線程會光柵化每個圖層。一個圖層可能會跟整個頁面一樣大,因此合成線程將它們分塊后發送到光柵線程。光柵線程光柵化每個小塊后會將它們存儲在顯存中。
raster圖17:光柵線程創建分塊的位圖并發送到 GPU
合成線程會給不同的光柵線程設置優先級,以便視窗(或附近)內的畫面可以先被光柵化。圖層還具有多個不同分辨率的塊,可以處理放大操作等動作。
一旦塊被光柵化,合成線程會收集這些塊的信息(稱為繪制四邊形)創建合成幀。
繪制四邊形
包含諸如圖塊在內存中的位置,以及合成時繪制圖塊在頁面中的位置等信息。
合成幀
一個繪制四邊形的集合,代表一個頁面的一幀。
接著,合成幀通過 IPC(進程間通訊)提交給瀏覽器進程。此時,可以從 UI 線程或其他插件的渲染進程添加另一個合成幀。這些合成器幀被發送到 GPU 然后在屏幕上顯示。如果接收到滾動事件,合成線程會創建另一個合成幀發送到 GPU。
composit圖 18:合成線程創建合成幀,將其發送到瀏覽器進程,再接著發送到 GPU
合成的好處是它可以在不涉及主線程的情況下完成。合成線程不需要等待樣式計算或 JavaScript 執行。這就是為什么僅合成動畫[16]被認為是流暢性能的最佳選擇。如果需要再次計算布局或繪制,則必須涉及主線程。
總結
在這篇文章中,我們研究了渲染管道從解析到合成的整個過程,希望現在你能自主地去了解更多關于網站性能優化的信息。
在本系列的下一篇也是最后一篇文章中,我們將更詳細地介紹合成線程,看看當用戶移動或點擊鼠標時會發生什么。
你喜歡這篇文章嗎?如果你對之后的文章有任何問題或建議,我很樂意在下面的評論部分或推特 @kosamari?與你聯系。
- 原文:Inside look at modern web browser (part 3)[17]
- 作者:@Mariko Kosaka
- 譯文:現代瀏覽器內部揭秘(第三部分)[18]
- 譯者:@ssshooter
- 校對者:@ThomasWhyne, @CoolRice
參考資料
[1]the Performance section of Web Fundamentals: https://developers.google.com/web/fundamentals/performance/why-performance-matters/
[2]HTML Standard: https://html.spec.whatwg.org/
[3]An introduction to error handling and strange cases in the parser: https://html.spec.whatwg.org/multipage/parsing.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser
[4]overview of the parsing model: https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model
[5]V8 團隊就此發表的演講和博客文章: https://mathiasbynens.be/notes/shapes-ics
[6]async: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async
[7]defer: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer
[8]JavaScript 模塊: https://developers.google.com/web/fundamentals/primers/modules
[9]Resource Prioritization – Getting the Browser to Help You: https://developers.google.com/web/fundamentals/performance/resource-prioritization
[10]可以在這里看到源代碼: https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/css/html.css
[11]BlinkOn 會議的一些訪談: https://www.youtube.com/watch?v=Y5Xa4H2wtVA
[12]Optimize JavaScript Execution: https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution
[13]在 Web Worker 中運行 JavaScript: https://www.youtube.com/watch?v=X57mh8tKkgE
[14]Layers 面板: https://blog.logrocket.com/eliminate-content-repaints-with-the-new-layers-panel-in-chrome-e2c306d4d752?gi=cd6271834cea
[15]Stick to Compositor-Only Properties and Manage Layer Count: https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count
[16]僅合成動畫: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
[17]Inside look at modern web browser (part 3): https://developers.google.com/web/updates/2018/09/inside-browser-part3
[18]現代瀏覽器內部揭秘(第三部分): https://juejin.im/post/6844903692894732295
看完三件事
如果你覺得本文對你有幫助,我想請你幫個忙:
公眾號后臺回復「加群」,加入算法和技術交流群,與更多讀者交流。
?
總結
以上是生活随笔為你收集整理的奔图内部扫描错误13_现代浏览器内部揭秘(第三部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: win10电脑pppoe拨号模块损坏_电
- 下一篇: c++ 一个函数包括多个返回值判断_轻松