您好,登錄后才能下訂單哦!
Chrome 算是程序員的標配了,從全球的市場份額來看,它在全球市場的份額已經超過 60%。
在 Chrome 10 周年之際,官方發布了一個系列文章,用圖解的方式,很清晰的講解了現代瀏覽器的運行原理。
渲染器進程涉及到 Web 性能相關的多個方面,由于渲染器進程中處理了很多的邏輯,不是一篇文章可以全面講解的,因此本文僅作為一個概述。如果你有興趣深入研究,可以在《Why Performance Matters》這篇文章里找到更多的資料。
所有選項卡內發生的邏輯,都由渲染器進程負責。在渲染器進程中,主線程處理了服務器發送給用戶的大部分代碼。如果你使用到 Web Workder 或者Service Worker,那 JavaScript 中的這部分代碼,將由工作線程處理。Compositor(合成器) 和 Raster(光柵) 線程也在渲染器內運行,從而實現高效、流暢的渲染頁面。
渲染器進程的核心工作是將 HTML,CSS 和 JavaScript 轉換為用戶可以與之交互的網頁。
上圖中,描述了具有主線程、工作線程、Compositor 線程、Raster 線程的渲染器進程,以及他們之間的關系。
當渲染器進程收到一個導航請求,并開始接收 HTML 數據,主線程將開始處理文本字符串(HTML),將其解析成 DOM(Document Object Model)。
DOM 是 Web 頁面的內部的邏輯樹文檔結構,Web 開發人員可以通過 JavaScript 腳本與之交互數據,以及通過標準 API 來操作 DOM 節點。
將 HTML 文檔解析成 DOM 是完全依照于 HTML 協議。并且在 HTML 協議中,瀏覽器不會對錯誤的 HTML 進行錯誤提示。例如,缺少結束的
</p>
標簽時,這依然是一個有效的 HTML。類似
Hi! <b>I'm <i>Chrome</b>!</i>
中,
b
標簽在
i
標簽之前關閉這樣的錯誤,會被 HTML 理解為
Hi! <b>I'm <i>Chrome</i></b><i>!</i>
。這是因為 HTML 規范的主要原則是優雅的處理這些錯誤,而不是嚴格檢查。
如果你對這些規范感到好奇,可以閱讀 HTML 規范中的 “解析器中的錯誤處理和奇怪案例介紹” 部分。
解析器中的錯誤處理和奇怪案例介紹:
https://html.spec.whatwg.org/multipage/parsing.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser
一個完整的 Web 站點通常會包含圖片、CSS 和 JS 等外部資源,這些文件都需要從網絡或者本地緩存中加載。主線程可以在解析構建 DOM 的時候,將他們逐個請求,但是為了加快速度,會同時使用 “預加載掃描(Preload Scanner)”。
如果 “預加載掃描” 發現有類似
<img>
或
<link>
這樣的標簽時,會由 HTML 解析器對該資源生成一個 Tokens,然后在瀏覽器進程中,通過網絡或者本地緩存來加載資源。
上圖描述了,主線程解析 HTML 并構建 DOM 樹的過程。
自己是一個五年的前端工程師
這里推薦一下我的前端學習交流群:731771211,里面都是學習前端的,如果你想制作酷炫的網頁,想學習編程。從最基礎的HTML+CSS+JS【炫酷特效,游戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小伙伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小伙伴歡迎加入。 點擊: 加入
當 HTML 解析器遇到 <script> 標簽的時候,它會暫停解析 HTML 文檔,然后對這個 JS 腳本進行加載、解析和執行。
這么設計的原因,是因為 JS 可以使用類似 document.write() 方法來改變 DOM 的結構。這就是 HTML 解析器在重新解析 HTML 之前,必須等待 JS 腳本執行的原因。
如果你對 JS 執行中發生的事情細節有興趣,V8 團隊有一篇文章深入的對此進行了講解,有興趣可以看看。
V8 團隊深入研究:
https://mathiasbynens.be/notes/shapes-ics
HTML 遇到 JS 腳本則暫停對 HTML 的解析,這并不是絕對的。
Web 開發人員可以通過多種方式的配置,告知瀏覽器如何更優雅的加載資源。如果你的 JS 腳本中,沒有使用到類似
document.write()
這樣的方法,你可以在
script
標簽中添加
async
或
defer
標記,然后瀏覽器會異步加載和運行此 JS 腳本,不會阻斷解析。如果需要,也可以使用 JavaScript Modules,還可以通過
<link rel="preload">
標簽向瀏覽器明確標記此為重要的資源,將在頁面加載完成之后被立刻使用,對于這類資源,它會在頁面加載生命周期的早期,被優先加載。
僅僅解析成 DOM,還不足以完成頁面渲染,因為還可以通過在 CSS 中,設置元素的樣式來豐富渲染效果。
主線程將解析 CSS,并將效果渲染到指定的 DOM 節點上,關于 CSS 選擇器如何定位到指定的 DOM 節點,可以通過 DevTools 來查看相關信息。
上圖中,主線程解析 CSS 并添加渲染樣式。
即使你不使用任何 CSS 樣式,每個 DOM 節點依然存在默認的渲染樣式。例如, h2 標簽在視覺上就大于 h3 標簽,并且每個元素還有默認的邊距。這是因為瀏覽器具有默認樣式表。
如果你對 Chrome 的默認 CSS 是什么樣的有興趣,可以在源碼中看到具體細節。
Chrome 的默認 CSS:
https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/css/html.css
到現在,渲染器進程知道每個 DOM 的結構和樣式了,但是這依然不足以渲染頁面。想象一下,你正視圖通過文字向朋友描述一副畫,“有一個大的紅色圓圈和一個小的藍色方塊”,這些信息不足以讓你的朋友還原這幅畫。
這就牽扯到布局(Layout),布局是對元素定位的過程,主線程遍歷 DOM 并計算樣式,然后創建布局樹(Layout Tree),在布局樹中,包含 X、Y 坐標和邊框大小等信息。布局樹是一個與 DOM 樹類似的結構,但是它僅僅包含了頁面上可見內容相關的信息。
舉個例子,如果某個元素設置了
display:none
,則該元素將不會出現在布局樹中,但是它會出現在 DOM 樹中,而如果該元素被設置為
visibility:hidden
則它會存在于布局樹中。類似的例子還有
p::before{content:"Hi!"}
這樣的偽類,它會存在于布局樹中,而不會存在于 DOM 樹中。
如上圖所示,在主線程中渲染樣式,并生成布局樹和 DOM 樹。
計算頁面布局是一個很復雜的工作,即使最簡單的從上到下的塊流結構,也必須考慮字體的大小以及如何劃分每一塊,因為它們會影響當前段落的大小和形狀,然后影響下一塊所在的位置。
CSS 樣式可以設置元素浮動到某一側、隱藏 overflow 的元素,或者改變排版方向。布局是一個非常復雜的工作,在 Chrome 中,有一個完整的工程師團隊負責布局。如果你的對他們工作的細節感興趣,可以參閱 BinkOn 會議的記錄。
BinkOn:
https://www.youtube.com/watch?v=Y5Xa4H2wtVA
擁有 DOM、CSS 和 LayoutTree 仍然不足以渲染頁面。假設你正在嘗試重繪一幅畫,你除了需要知道元素的大小、外觀和位置之外,還需要知道它們的繪制順序。
例如:
z-index
屬性將改變元素的層級,在這種情況下,按 HTML 中編寫的元素順序進行繪制,將導致渲染結果和預期不符。
如上圖所示,因為沒有正確的考慮
z-index
,將導致頁面被錯誤的渲染。
在這個繪制的過程中,主線程遍歷布局樹,然后創建繪制記錄。繪制記錄是一個繪制過程的注釋,例如“背景優先,然后是文本,最后是矩形”。如果你曾經使用 JS 在
<canvas>
上繪制元素,那么你對此過程應該會很熟悉。
如上圖所示,主線程遍歷布局樹,并生成繪制記錄。
渲染管道(Rendering Pipeline)中最重要的任務,就是在每個步驟開始前,根據前一次操作的結果,來創建新的數據。例如,如果布局樹中的某些內容發生更改,則需要為文檔的受影響部分重新生成“繪制”順序。
渲染管道(Rendering Pipeline)中最重要的任務,就是在每個步驟開始前,根據前一次操作的結果,來創建新的數據。例如,如果布局樹中的某些內容發生變動,則需要為文檔中受影響的部分,重新生成“繪制記錄”。
為元素設置的動畫,瀏覽器必須在每一幀之間執行這些操作。我們大多數顯示器每秒刷新 60 次(60fps),如果你對每一幀都做了處理,那動畫對人眼而言就是平滑的,但是如果某些幀沒有被處理到或者丟失了,則會導致動畫不連貫,出現頁面的“卡頓”。
哪怕渲染的計算可以跟上屏幕刷新,可因為此計算過程發生在主線程上,當執行 JavaScript 腳本時,可能導致渲染過程被阻斷。
即使渲染的計算可以跟上屏幕的刷新速度,可因為此計算是在主線程上執行的,這就意味著 JS 代碼的執行,也可能導致它被阻斷。
如上圖,時間軸上的動畫幀,被 JS 阻止了一幀。
為此,你可以將 JavaScript 操作劃分成小塊,并在每幀上執行 requestAnimationFrame() ,還可以在 Web Workers 中運行 JavaScript,以避免阻塞主線程。
如圖所示,在動畫幀的時間軸上,運行較小的 JavaScript 塊。
現在瀏覽器知道文檔的結構,每個元素的樣式,頁面的形狀和繪制順序,它是如何繪制頁面的?將此信息轉換為屏幕上的像素稱為光柵化(rasterizing)。
光柵化是將幾何數據經過一系列變換后最終轉換為像素,從而呈現在顯示設備上的過程。
也許處理這種情況的一種無腦方案,是在視口(ViewPort)內部將每個組件都光柵化。如果用戶滾動頁面,則移動光柵幀,并通過更多光柵元素填充缺少的部分。
這就是 Chrome 首次發布時處理光柵化的方式,但是,現代瀏覽器運行一個更復雜的被稱為合成(Compositing)的進程。
合成是一種將頁面的各個元素進行分層,分別光柵化,并在合成器線程中以一個單獨的線程合成新頁面的技術。如果頁面發生滾動,由于圖層已經光柵化,因此它需要做的就是合成一個新幀。通過移動圖層同時合成新幀,可以以相同的方式實現動畫。
你可以在 DevTools 中的 Layout panel 來查看看圖層。
分層
為了確定每個元素所在的層,主線程遍歷布局樹以創建層樹(Layer Tree)。如果頁面的某元素應該是一個單獨的圖層(例如側滑菜單),那么你可以在 CSS 中,使用 will-change 屬性提示瀏覽器。
如上圖,在主線程中遍歷布局樹,并生成層樹。
雖然理想情況下,應該為每個元素生成圖層,但是對過多的小圖層進行合并,可能會比對頁面的每幀上柵格化小元素更慢,因此測量應用程序的渲染性能就非常重要。有關主題的更多信息,請參閱 Stick to Compositor-Only Properties 和 Manage Layer Count。
Stick to Compositor-Only Properties 和 Manage Layer Count: https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count
一旦創建了層樹并確定了繪制順序,主線程就會將該信息提交給合成器線程。合成器線程會光柵化每個圖層,一個圖層可能想一個完整的頁面那么大,因此合成器線程將他們分成圖塊,并將每個圖塊發送到光柵線程。光柵線程格式化每個元素,并將他們存儲在 GPU 內存中。
合成器線程可以優先考慮不同的光柵線程,以便 ViewPort(或附近)的元素可以被優先光柵化。圖層還具有多個不同分辨率的傾斜度,以便對內容的放大等操作。
一旦元素被光柵化,合成器線程會收集被稱為 “繪制矩形(Draw Quads)” 的信息,用以創建一個合成幀(Compositor Frame)。
然后通過 IPC 將合成幀提交給瀏覽器進程。此時,可以從 UI 線程添加另一個合成幀用于瀏覽器的 UI 更新,或者從其他渲染器進程中添加擴展。這些合成幀被發送到 GPU 中,用以在屏幕上顯示。如果觸發滾動事件,合成器線程會創建另一個合成幀發送到 GPU。
上圖中,合成器線程創建合成幀。將此幀發送到瀏覽器進程然后發送到 GPU。
合成(Compositor)的好處,是它可以在不影響主線程的情況下完成。合成器線程不需要等待樣式計算或者 JS 腳本執行,這就是為什么 “僅合成動畫” 被認為是平滑性能的最佳選擇。如果需要再次計算不會或者重新繪制,則必須涉及到主線程。
小結
在這篇文章中,我們研究了從解析到合成的渲染流程,更多關于網站優化問題可以關注一下。
自己是一個五年的前端工程師
這里推薦一下我的前端學習交流群:731771211,里面都是學習前端的,如果你想制作酷炫的網頁,想學習編程。從最基礎的HTML+CSS+JS【炫酷特效,游戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小伙伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小伙伴歡迎加入。 點擊: 加入
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。