HTML/CSS/JS 是如何在瀏覽器中渲染成你看到的頁面?【圖解Chrome】

智雲程式設計發表於2019-01-05

Chrome 算是程式設計師的標配了,從全球的市場份額來看,它在全球市場的份額已經超過 60%。

在 Chrome 10 週年之際,官方釋出了一個系列文章,用圖解的方式,很清晰的講解了現代瀏覽器的執行原理。

渲染器程式涉及到 Web 效能相關的多個方面,由於渲染器程式中處理了很多的邏輯,不是一篇文章可以全面講解的,因此本文僅作為一個概述。如果你有興趣深入研究,可以在《Why Performance Matters》這篇文章裡找到更多的資料。

渲染器程式處理Web內容

所有選項卡內發生的邏輯,都由渲染器程式負責。在渲染器程式中,主執行緒處理了伺服器傳送給使用者的大部分程式碼。如果你使用到 Web Workder 或者Service Worker,那 JavaScript 中的這部分程式碼,將由工作執行緒處理。Compositor(合成器) 和 Raster(光柵) 執行緒也在渲染器內執行,從而實現高效、流暢的渲染頁面。

渲染器程式的核心工作是將 HTML,CSS 和 JavaScript 轉換為使用者可以與之互動的網頁。

image.png
image.png

上圖中,描述了具有主執行緒、工作執行緒、Compositor 執行緒、Raster 執行緒的渲染器程式,以及他們之間的關係。

解析

構建 DOM

當渲染器程式收到一個導航請求,並開始接收 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,然後在瀏覽器程式中,通過網路或者本地快取來載入資源。

image.png
image.png

上圖描述了,主執行緒解析 HTML 並構建 DOM 樹的過程。

自己是一個五年的前端工程師


這裡推薦一下我的前端學習交流群:731771211,裡面都是學習前端的,如果你想製作酷炫的網頁,想學習程式設計。從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入。 點選:  加入

JS 可以阻止解析

當 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">  標籤向瀏覽器明確標記此為重要的資源,將在頁面載入完成之後被立刻使用,對於這類資源,它會在頁面載入生命週期的早期,被優先載入。

樣式渲染(Style)

僅僅解析成 DOM,還不足以完成頁面渲染,因為還可以通過在 CSS 中,設定元素的樣式來豐富渲染效果。

主執行緒將解析 CSS,並將效果渲染到指定的 DOM 節點上,關於 CSS 選擇器如何定位到指定的 DOM 節點,可以通過 DevTools 來檢視相關資訊。

image.png
image.png

上圖中,主執行緒解析 CSS 並新增渲染樣式。

即使你不使用任何 CSS 樣式,每個 DOM 節點依然存在預設的渲染樣式。例如, h1 標籤在視覺上就大於 h2 標籤,並且每個元素還有預設的邊距。這是因為瀏覽器具有預設樣式表。

如果你對 Chrome 的預設 CSS 是什麼樣的有興趣,可以在原始碼中看到具體細節。

Chrome 的預設 CSS:
https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/css/html.css

佈局(Layout)

到現在,渲染器程式知道每個 DOM 的結構和樣式了,但是這依然不足以渲染頁面。想象一下,你正檢視通過文字向朋友描述一副畫,“有一個大的紅色圓圈和一個小的藍色方塊”,這些資訊不足以讓你的朋友還原這幅畫。

image.png
image.png

這就牽扯到佈局(Layout),佈局是對元素定位的過程,主執行緒遍歷 DOM 並計算樣式,然後建立佈局樹(Layout Tree),在佈局樹中,包含 X、Y 座標和邊框大小等資訊。佈局樹是一個與 DOM 樹類似的結構,但是它僅僅包含了頁面上可見內容相關的資訊。

舉個例子,如果某個元素設定了 display:none ,則該元素將不會出現在佈局樹中,但是它會出現在 DOM 樹中,而如果該元素被設定為  visibility:hidden  則它會存在於佈局樹中。類似的例子還有  p::before{content:"Hi!"} 這樣的偽類,它會存在於佈局樹中,而不會存在於 DOM 樹中。

image.png
image.png

如上圖所示,在主執行緒中渲染樣式,並生成佈局樹和 DOM 樹。

計算頁面佈局是一個很複雜的工作,即使最簡單的從上到下的塊流結構,也必須考慮字型的大小以及如何劃分每一塊,因為它們會影響當前段落的大小和形狀,然後影響下一塊所在的位置。

image.png
image.png

CSS 樣式可以設定元素浮動到某一側、隱藏 overflow 的元素,或者改變排版方向。佈局是一個非常複雜的工作,在 Chrome 中,有一個完整的工程師團隊負責佈局。如果你的對他們工作的細節感興趣,可以參閱 BinkOn 會議的記錄。

BinkOn:
https://www.youtube.com/watch?v=Y5Xa4H2wtVA

繪製(Paint)

擁有 DOM、CSS 和 LayoutTree 仍然不足以渲染頁面。假設你正在嘗試重繪一幅畫,你除了需要知道元素的大小、外觀和位置之外,還需要知道它們的繪製順序。

image.png
image.png

例如: z-index 屬性將改變元素的層級,在這種情況下,按 HTML 中編寫的元素順序進行繪製,將導致渲染結果和預期不符。

image.png
image.png

如上圖所示,因為沒有正確的考慮 z-index ,將導致頁面被錯誤的渲染。

在這個繪製的過程中,主執行緒遍歷佈局樹,然後建立繪製記錄。繪製記錄是一個繪製過程的註釋,例如“背景優先,然後是文字,最後是矩形”。如果你曾經使用 JS 在  <canvas> 上繪製元素,那麼你對此過程應該會很熟悉。

image.png
image.png

如上圖所示,主執行緒遍歷佈局樹,並生成繪製記錄。

更新渲染管道的成本很高

渲染管道(Rendering Pipeline)中最重要的任務,就是在每個步驟開始前,根據前一次操作的結果,來建立新的資料。例如,如果佈局樹中的某些內容發生更改,則需要為文件的受影響部分重新生成“繪製”順序。

渲染管道(Rendering Pipeline)中最重要的任務,就是在每個步驟開始前,根據前一次操作的結果,來建立新的資料。例如,如果佈局樹中的某些內容發生變動,則需要為文件中受影響的部分,重新生成“繪製記錄”。

image.png
image.png

為元素設定的動畫,瀏覽器必須在每一幀之間執行這些操作。我們大多數顯示器每秒重新整理 60 次(60fps),如果你對每一幀都做了處理,那動畫對人眼而言就是平滑的,但是如果某些幀沒有被處理到或者丟失了,則會導致動畫不連貫,出現頁面的“卡頓”。

image.png
image.png

哪怕渲染的計算可以跟上螢幕重新整理,可因為此計算過程發生在主執行緒上,當執行 JavaScript 指令碼時,可能導致渲染過程被阻斷。

即使渲染的計算可以跟上螢幕的重新整理速度,可因為此計算是在主執行緒上執行的,這就意味著 JS 程式碼的執行,也可能導致它被阻斷。

image.png
image.png

如上圖,時間軸上的動畫幀,被 JS 阻止了一幀。

為此,你可以將 JavaScript 操作劃分成小塊,並在每幀上執行 requestAnimationFrame() ,還可以在 Web Workers 中執行 JavaScript,以避免阻塞主執行緒。

image.png
image.png

如圖所示,在動畫幀的時間軸上,執行較小的 JavaScript 塊。

合成(Compositing)

如何繪製一個頁面?

現在瀏覽器知道文件的結構,每個元素的樣式,頁面的形狀和繪製順序,它是如何繪製頁面的?將此資訊轉換為螢幕上的畫素稱為光柵化(rasterizing)。

光柵化是將幾何資料經過一系列變換後最終轉換為畫素,從而呈現在顯示裝置上的過程。

305.gif
305.gif

也許處理這種情況的一種無腦方案,是在視口(ViewPort)內部將每個元件都光柵化。如果使用者滾動頁面,則移動光柵幀,並通過更多光柵元素填充缺少的部分。

這就是 Chrome 首次釋出時處理光柵化的方式,但是,現代瀏覽器執行一個更復雜的被稱為合成(Compositing)的程式。

什麼是合成(Compositing)

合成是一種將頁面的各個元素進行分層,分別光柵化,並在合成器執行緒中以一個單獨的執行緒合成新頁面的技術。如果頁面發生滾動,由於圖層已經光柵化,因此它需要做的就是合成一個新幀。通過移動圖層同時合成新幀,可以以相同的方式實現動畫。

HTML/CSS/JS 是如何在瀏覽器中渲染成你看到的頁面?【圖解Chrome】

你可以在 DevTools 中的 Layout panel 來檢視看圖層。

分層

為了確定每個元素所在的層,主執行緒遍歷佈局樹以建立層樹(Layer Tree)。如果頁面的某元素應該是一個單獨的圖層(例如側滑選單),那麼你可以在 CSS 中,使用 will-change 屬性提示瀏覽器。

image.png
image.png

如上圖,在主執行緒中遍歷佈局樹,並生成層樹。

雖然理想情況下,應該為每個元素生成圖層,但是對過多的小圖層進行合併,可能會比對頁面的每幀上柵格化小元素更慢,因此測量應用程式的渲染效能就非常重要。有關主題的更多資訊,請參閱 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 記憶體中。

光柵執行緒建立光柵點陣圖併傳送到GPU
光柵執行緒建立光柵點陣圖併傳送到GPU

合成器執行緒可以優先考慮不同的光柵執行緒,以便 ViewPort(或附近)的元素可以被優先光柵化。圖層還具有多個不同解析度的傾斜度,以便對內容的放大等操作。

一旦元素被光柵化,合成器執行緒會收集被稱為 “繪製矩形(Draw Quads)” 的資訊,用以建立一個合成幀(Compositor Frame)。

然後通過 IPC 將合成幀提交給瀏覽器程式。此時,可以從 UI 執行緒新增另一個合成幀用於瀏覽器的 UI 更新,或者從其他渲染器程式中新增擴充套件。這些合成幀被髮送到 GPU 中,用以在螢幕上顯示。如果觸發滾動事件,合成器執行緒會建立另一個合成幀傳送到 GPU。

image.png
image.png

上圖中,合成器執行緒建立合成幀。將此幀傳送到瀏覽器程式然後傳送到 GPU。

合成(Compositor)的好處,是它可以在不影響主執行緒的情況下完成。合成器執行緒不需要等待樣式計算或者 JS 指令碼執行,這就是為什麼 “僅合成動畫” 被認為是平滑效能的最佳選擇。如果需要再次計算不會或者重新繪製,則必須涉及到主執行緒。

小結

在這篇文章中,我們研究了從解析到合成的渲染流程,更多關於網站優化問題可以關注一下。

自己是一個五年的前端工程師


這裡推薦一下我的前端學習交流群:731771211,裡面都是學習前端的,如果你想製作酷炫的網頁,想學習程式設計。從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入。 點選:  加入


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901074/viewspace-2331172/,如需轉載,請註明出處,否則將追究法律責任。

相關文章