深刻了解現代瀏覽器之三 - 渲染

無心間在 Google Developer 上看到的文章,這是這個系列博客的 第三部分,主要是研究渲染進程所作的事情。渲染進程涉及到 Web 性能的不少方面,這裏只是概述,若是你想深刻了解,能夠去 Web 基礎的性能部分看看。

渲染進程處理網頁內容

渲染進程負責處理標頁內的全部事情。其中,主線程負責處理大部分代碼。少部分的代碼可能會由工做線程處理(好比 Service Worker 或者 Web Worker)。同時,合成器線程和柵格線程也在渲染進程中運行,負責高效、流暢的呈現頁面。javascript

渲染進程具備主線程、工做線程、合成器線程和柵格線程

解析數據

構造 DOM 樹

渲染進程接收到導航提交的消息後,就開始接收 HTML 數據,主線程就開始解析文本字符串(HTML),並將其轉換成 DOM(Document Object Model)。css

DOM 是頁面在瀏覽器內部的結構,也是開發人員經過 JavaScript 與之交互的數據結構和 API。html

解析 HTML 的規則由 HTML 標準定義。同時 HTML 標準要求兼容錯誤的寫法,若是你對這個感興趣,能夠查看 An introduction to error handling and strange cases in the parser 的 HTML 部分。html5

子資源加載

一個網站常常會使用一些外部資源,好比 CSS、圖片以及 JavaScript 等。這些文件都須要從網絡獲取或者是從緩存中加載。主線程在解析構建 DOM 時,會發現一個加載一個,可是這樣太慢,因而爲了加快速度,「預加載掃描器」會同時運行。當在文檔中發現有像 <img> 或者 <link> 的內容時,預加載掃描器會將請求提交給瀏覽器進程中的網絡線程。java

主線程解析 HTML 並構建 DOM 樹

JavaScript 可能阻塞解析

若是解析器碰到了 <script> 標籤,就會暫停解析 HTML 文檔,而後開始解析和執行 JavaScript 代碼。爲何呢?由於 JavaScript 可能會經過 document.write() 這樣的代碼修改文檔,從而改變 DOM 結構(HTML 標準裏有張解析模型的圖很是好)。因此 HTML 解析器就必需要停下來執行 JavaScript,而後再繼續解析 HTML。若是你對 JavaScript 執行的細節感興趣,能夠看看 V8 團隊的分享web

提示瀏覽器如何加載資源

Web 開發者能夠經過多種方式提示瀏覽器。若是你的 JavaScript 代碼不使用 document.write(),就能夠在 <script> 標籤上添加 async 或者 defer 屬性,這樣瀏覽器就會異步加載運行 JavaScript 代碼,而不會阻塞解析。若是能夠的話,也可使用 JavaScript 模塊。可使用 <link rel="preload"> 告訴瀏覽器當前導航確定須要該資源,但願儘快下載。有關信息請參閱資源優先級canvas

樣式計算

光一個 DOM 結構,咱們仍是不知道頁面長啥樣子,咱們還須要 CSS 來設置頁面元素的樣式。因此主線程會解析 CSS 來計算每一個 DOM 節點長什麼樣子。基於 CSS 選擇器,對每一個元素應用相應的樣式,這些均可以在 DevTools 中的 computed 中看到。瀏覽器

主線程解析 CSS 並計算樣式

即使你不提供任何 CSS,每一個 DOM 節點都會有樣式。好比 <h1> 顯示出來比 <h2> 大,而且每一個元素都有邊距。這是由於瀏覽器具備默認樣式,若是你想知道 Chrome 默認的樣式,能夠到這裏看源代碼緩存

佈局

如今渲染進程知道了文檔的結構和每一個節點的樣式,但仍是不足以渲染頁面。想象一下,你給你朋友打電話描述一幅畫:「畫裏有一個大紅圈和一個藍色小方塊。」,你的朋友聽了你的描述,可能仍是一臉懵逼。網絡

佈局就是計算出元素之間的幾何位置的過程。主線程會遍歷 DOM 樹和樣式,而後構造出一顆佈局樹,這棵樹上的節點都帶有 x、y 座標和邊界框大小之類的信息。佈局樹和 DOM 樹的結構相似,可是樹上只包含頁面可見元素的信息。若是元素被設置了 display: none,那麼佈局樹就不會包含這個元素(visibility: hidden 的元素會被包含)。一樣的,若是一個內容是經過僞類(好比 p::before { content: 'Hi!' })添加進來的,那麼這個元素會被包含在佈局樹中,可是 DOM 樹中沒有。

主線程遍歷具備樣式的 DOM 樹生成佈局樹

因爲換行而更改的佈局

肯定頁面如何佈局是一項很是難的事情。即便是最簡單的佈局方式也要考慮字體大小、換行之類的事情,更別說浮動、隱藏溢出、修改文本顯示方向等等事情了。在 Chrome 裏,有一個專門負責佈局的團隊,感興趣的話,能夠看看這個分享

繪製

先畫圓仍是先畫方?

有了 DOM 結構、樣式、佈局以後,咱們仍是不能渲染頁面,咱們還要解決渲染的順序問題。好比,有些元素可能設置了 z-index 屬性,那麼按照 HTML 裏面的元素順序進行渲染就會出錯。

沒考慮 z-index 而致使渲染錯誤

因此在這一步,主線程會遍歷佈局樹,並建立繪製記錄。繪製記錄會記錄繪製過程,就像是先畫背景,再畫文本,最後畫矩形。若是你用過 canvas,那麼你可能對這個過程會很熟悉。

主線程遍歷佈局樹生成繪製記錄

更新渲染管道的成本很高

渲染的過程是一個流水線,每一個步驟的結果都用於下一個步驟。若是佈局樹變化了,那麼就須要從新爲受影響的部分生成繪製記錄。

DOM 樹、佈局樹、繪製記錄的生成順序

若是要給元素設置動畫,瀏覽器就要在每一幀運行這些操做。大多數的顯示器屏幕每秒刷新 60 次(60 fps),當每一幀都在變化的時候,人就會以爲動畫很流暢,可是,若是中間丟了一些幀就會顯得很卡頓。

時間軸上的動畫幀

即使渲染能跟得上屏幕刷新,但動畫是在主線程上進行計算,也就是說若是主線程一旦由於執行 JavaScript 代碼而被阻塞了,動畫也就被卡住了。

動畫被 JavaScript 阻塞了

你能夠將動畫涉及的 JavaScript 操做分紅小塊,並使用 requestAnimationFrame() 調度在每一幀上執行,更多請參考。你也能夠在 Web Worker 中運行 JavaScript以免阻塞主線程。

合成

如何繪製頁面?

簡單柵格化的處理動畫

如今瀏覽器知道了文檔結構、元素的樣式、頁面的幾何關係以及繪製順序,接下來就該渲染頁面了。具體該怎麼渲染呢?把上述信息轉換成屏幕上的像素叫作柵格化。

最簡單的處理方式就是把頁面在當前視窗中的部分先轉換成像素。若是用戶滾動頁面,則移動柵格化的畫框,填補沒有渲染的部分。Chrome 最先就是這麼幹的,但現代瀏覽器有更復雜的流程,叫作合成。

合成過程的動畫

合成是將頁面的各個部分進行分層,而後分別對其進行柵格化,而後經過單獨的線程進行合成的技術。這樣的話,當用戶滾動頁面的時候,由於圖層都被柵格化了,因此瀏覽器只須要合成一個新的幀便可。動畫也能夠經過移動圖層再合成新的幀來實現。

你能夠在 DevTools 裏經過 Layers 面板查看網站的分層(能夠在開發者工具裏找到)。

分層

爲了找出哪些元素在那個圖層,主線程會遍歷佈局樹來建立圖層樹。若是頁面的某些部分是單獨的圖層(好比滑入式側邊菜單)可是沒有拆分出來,你能夠用 CSS 裏的 will-change 屬性來提示瀏覽器進行拆分。

主線程遍歷佈局樹生成圖層樹

分層並非越多越好,層過多可能會形成操做速度變慢,甚至還不如每幀都對頁面中的小部分執行一次柵格化快,至於該怎麼平衡,能夠參考這裏

主線程的柵格化和合成

一旦建立了圖層樹,並肯定了繪製的順序,主線程就會將信息提交給合成線程。緊接着,合成線程會柵格化每一個圖層。有的狀況下一個圖層可能和頁面同樣長,所以合成線程會將它們劃分紅圖塊後發送給柵格線程。柵格線程柵格化每一個圖塊(圖塊轉化爲位圖),並將它們存到顯存中。

柵格線程建立圖塊的位圖發送到 GPU

合成線程會根據柵格線程不一樣的優先級處理圖塊,好比它會優先處理視窗(及附近)的圖塊。而且圖塊還具備不一樣分辨率的圖塊,以便在用戶放大、縮放時使用。

全部的圖塊都柵格化後,合成線程會收集這些圖塊的信息(繪製圖塊)來建立合成幀。

  • 繪製圖塊:包含圖塊在內存中的地址、頁面中的位置等相關信息
  • 合成幀:多個繪製圖塊的集合,繪成了頁面的一幀

建立好的合成幀會經過 IPC 提交給瀏覽器進程。此時,能夠從 UI 線程或者其餘插件的渲染進程添加另外一個合成幀。這些合成幀會被髮送到 GPU 進行,最終展現到屏幕上。若是發生了滾動,合成線程會建立另外一個合成幀發送給 GPU。

合成線程將合成幀發送到瀏覽器進程,而後發送到 GPU

合成的好處就是和主線程無關。合成線程不須要等待樣式計算或者 JavaScript 的執行,這也是爲何只須要合成的動畫流暢平滑的緣由。若是須要再次計算佈局或者繪製,就須要涉及到主線程了(這就是爲何要減小重排和重繪)。

後續還會有接下來的最後一篇 - 交互,公衆號裏有上兩篇的內容,歡迎關注、轉發、分享支持我。

公衆號

相關文章
相關標籤/搜索