從 8 道面試題看瀏覽器渲染過程與性能優化

chrome (1).jpeg

前言

移動互聯網時代,用戶對於網頁的打開速度要求愈來愈高。百度用戶體驗部研究代表,頁面放棄率和頁面的打開時間關係以下圖 所示。css

chart

charthtml

根據百度用戶體驗部的研究結果來看,普通用戶指望且可以接受的頁面加載時間在 3 秒之內。若頁面的加載時間過慢,用戶就會失去耐心而選擇離開。前端

首屏做爲直面用戶的第一屏,其重要性不言而喻。優化用戶體驗更是咱們前端開發很是須要 focus 的東西之一。html5

本文咱們經過 8 道面試題來聊聊瀏覽器渲染過程與性能優化。node

咱們首先帶着這 8 個問題,來了解瀏覽器渲染過程,後面會給出題解~git

  1. 爲何 Javascript 要是單線程的 ?
  2. 爲何 JS 阻塞頁面加載 ?
  3. css 加載會形成阻塞嗎 ?
  4. DOMContentLoaded 與 load 的區別 ?
  5. 什麼是 CRP,即關鍵渲染路徑(Critical Rendering Path)? 如何優化 ?
  6. defer 和 async 的區別 ?
  7. 談談瀏覽器的迴流與重繪 ?
  8. 什麼是渲染層合併 (Composite) ?

進程 (process) 和線程 (thread)

進程(process)和線程(thread)是操做系統的基本概念。github

進程是 CPU 資源分配的最小單位(是能擁有資源和獨立運行的最小單位)。web

線程是 CPU 調度的最小單位(是創建在進程基礎上的一次程序運行單位)。面試

process_thread

process_threadchrome

現代操做系統都是能夠同時運行多個任務的,好比:用瀏覽器上網的同時還能夠聽音樂。

對於操做系統來講,一個任務就是一個進程,好比打開一個瀏覽器就是啓動了一個瀏覽器進程,打開一個 Word 就啓動了一個 Word 進程。

有些進程同時不止作一件事,好比 Word,它同時能夠進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時作多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程

因爲每一個進程至少要作一件事,因此一個進程至少有一個線程。系統會給每一個進程分配獨立的內存,所以進程有它獨立的資源。同一進程內的各個線程之間共享該進程的內存空間(包括代碼段,數據集,堆等)。

借用一個生動的比喻來講,進程就像是一個有邊界的生產廠間,而線程就像是廠間內的一個個員工,能夠本身作本身的事情,也能夠相互配合作同一件事情。

當咱們啓動一個應用,計算機會建立一個進程,操做系統會爲進程分配一部份內存,應用的全部狀態都會保存在這塊內存中。

應用也許還會建立多個線程來輔助工做,這些線程能夠共享這部份內存中的數據。若是應用關閉,進程會被終結,操做系統會釋放相關內存。

process_thread_example

process_thread_example

瀏覽器的多進程架構

一個好的程序經常被劃分爲幾個相互獨立又彼此配合的模塊,瀏覽器也是如此。

以 Chrome 爲例,它由多個進程組成,每一個進程都有本身核心的職責,它們相互配合完成瀏覽器的總體功能,

每一個進程中又包含多個線程,一個進程內的多個線程也會協同工做,配合完成所在進程的職責。

Chrome 採用多進程架構,其頂層存在一個 Browser process 用以協調瀏覽器的其它進程。

process

process

優勢

因爲默認 新開 一個 tab 頁面 新建 一個進程,因此單個 tab 頁面崩潰不會影響到整個瀏覽器。

一樣,第三方插件崩潰也不會影響到整個瀏覽器。

多進程能夠充分利用現代 CPU 多核的優點。

方便使用沙盒模型隔離插件等進程,提升瀏覽器的穩定性。

缺點

系統爲瀏覽器新開的進程分配內存、CPU 等資源,因此內存和 CPU 的資源消耗也會更大。

不過 Chrome 在內存釋放方面作的不錯,基本內存都是能很快釋放掉給其餘程序運行的。

瀏覽器的主要進程和職責

process_list

process_list

主進程 Browser Process

負責瀏覽器界面的顯示與交互。各個頁面的管理,建立和銷燬其餘進程。網絡的資源管理、下載等。

第三方插件進程 Plugin Process

每種類型的插件對應一個進程,僅當使用該插件時才建立。

GPU 進程 GPU Process

最多隻有一個,用於 3D 繪製等

渲染進程 Renderer Process

稱爲瀏覽器渲染進程或瀏覽器內核,內部是多線程的。主要負責頁面渲染,腳本執行,事件處理等。 (本文重點分析)

渲染進程 (瀏覽器內核)

瀏覽器的渲染進程是多線程的,咱們來看看它有哪些主要線程 :

renderder_process

renderder_process

1. GUI 渲染線程

  • 負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹,佈局和繪製等。
  • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行。
  • 注意,GUI 渲染線程與 JS 引擎線程是互斥的,當 JS 引擎執行時 GUI 線程會被掛起(至關於被凍結了),GUI 更新會被保存在一個隊列中等到 JS 引擎空閒時當即被執行。

2. JS 引擎線程

  • Javascript 引擎,也稱爲 JS 內核,負責處理 Javascript 腳本程序。(例如 V8 引擎)
  • JS 引擎線程負責解析 Javascript 腳本,運行代碼。
  • JS 引擎一直等待着任務隊列中任務的到來,而後加以處理,一個 Tab 頁(renderer 進程)中不管何時都只有一個 JS 線程在運行 JS 程序。
  • 注意,GUI 渲染線程與 JS 引擎線程是互斥的,因此若是 JS 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。

3. 事件觸發線程

  • 歸屬於瀏覽器而不是 JS 引擎,用來控制事件循環(能夠理解,JS 引擎本身都忙不過來,須要瀏覽器另開線程協助)
  • 當 JS 引擎執行代碼塊如 setTimeOut 時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、AJAX 異步請求等),會將對應任務添加到事件線程中
  • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理
  • 注意,因爲 JS 的單線程關係,因此這些待處理隊列中的事件都得排隊等待 JS 引擎處理(當 JS 引擎空閒時纔會去執行)

4. 定時觸發器線程

  • 傳說中的 setInterval 與 setTimeout 所在線程
  • 瀏覽器定時計數器並非由 JavaScript 引擎計數的,(由於 JavaScript 引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確)
  • 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待 JS 引擎空閒後執行)
  • 注意,W3C 在 HTML 標準中規定,規定要求 setTimeout 中低於 4ms 的時間間隔算爲 4ms。

5. 異步 http 請求線程

  • 在 XMLHttpRequest 在鏈接後是經過瀏覽器新開一個線程請求。
  • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由 JavaScript 引擎執行。

瀏覽器渲染流程

若是要講從輸入 url 到頁面加載發生了什麼,那怕是沒完沒了了…這裏咱們只談談瀏覽器渲染的流程。

workflow

workflow

  1. 解析 HTML 文件,構建 DOM 樹,同時瀏覽器主進程負責下載 CSS 文件
  2. CSS 文件下載完成,解析 CSS 文件成樹形的數據結構,而後結合 DOM 樹合併成 RenderObject 樹
  3. 佈局 RenderObject 樹 (Layout/reflow),負責 RenderObject 樹中的元素的尺寸,位置等計算
  4. 繪製 RenderObject 樹 (paint),繪製頁面的像素信息
  5. 瀏覽器主進程將默認的圖層和複合圖層交給 GPU 進程,GPU 進程再將各個圖層合成(composite),最後顯示出頁面

題解

1. 爲何 Javascript 要是單線程的 ?

這是由於 Javascript 這門腳本語言誕生的使命所致!JavaScript 爲處理頁面中用戶的交互,以及操做 DOM 樹、CSS 樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。

若是 JavaScript 是多線程的方式來操做這些 UI DOM,則可能出現 UI 操做的衝突。

若是 Javascript 是多線程的話,在多線程的交互下,處於 UI 中的 DOM 節點就可能成爲一個臨界資源,

假設存在兩個線程同時操做一個 DOM,一個負責修改一個負責刪除,那麼這個時候就須要瀏覽器來裁決如何生效哪一個線程的執行結果。

固然咱們能夠經過鎖來解決上面的問題。但爲了不由於引入了鎖而帶來更大的複雜性,Javascript 在最初就選擇了單線程執行。

2. 爲何 JS 阻塞頁面加載 ?

因爲 JavaScript 是可操縱 DOM 的,若是在修改這些元素屬性同時渲染界面(即 JavaScript 線程和 UI 線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。

所以爲了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JavaScript 引擎爲互斥的關係。

當 JavaScript 引擎執行時 GUI 線程會被掛起,GUI 更新會被保存在一個隊列中等到引擎線程空閒時當即被執行。

從上面咱們能夠推理出,因爲 GUI 渲染線程與 JavaScript 執行線程是互斥的關係,

當瀏覽器在執行 JavaScript 程序的時候,GUI 渲染線程會被保存在一個隊列中,直到 JS 程序執行完成,纔會接着執行。

所以若是 JS 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。

3. css 加載會形成阻塞嗎 ?

由上面瀏覽器渲染流程咱們能夠看出 :

DOM 解析和 CSS 解析是兩個並行的進程,因此 CSS 加載不會阻塞 DOM 的解析

然而,因爲 Render Tree 是依賴於 DOM Tree 和 CSSOM Tree 的,

因此他必須等待到 CSSOM Tree 構建完成,也就是 CSS 資源加載完成(或者 CSS 資源加載失敗)後,才能開始渲染。

所以,CSS 加載會阻塞 Dom 的渲染

因爲 JavaScript 是可操縱 DOM 和 css 樣式 的,若是在修改這些元素屬性同時渲染界面(即 JavaScript 線程和 UI 線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。

所以爲了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JavaScript 引擎爲互斥的關係。

所以,樣式表會在後面的 js 執行前先加載執行完畢,因此css 會阻塞後面 js 的執行

4. DOMContentLoaded 與 load 的區別 ?

  • 當 DOMContentLoaded 事件觸發時,僅當 DOM 解析完成後,不包括樣式表,圖片。咱們前面提到 CSS 加載會阻塞 Dom 的渲染和後面 js 的執行,js 會阻塞 Dom 解析,因此咱們能夠獲得結論:
    當文檔中沒有腳本時,瀏覽器解析完文檔便能觸發 DOMContentLoaded 事件。若是文檔中包含腳本,則腳本會阻塞文檔的解析,而腳本須要等 CSSOM 構建完成才能執行。在任何狀況下,DOMContentLoaded 的觸發不須要等待圖片等其餘資源加載完成。
  • 當 onload 事件觸發時,頁面上全部的 DOM,樣式表,腳本,圖片等資源已經加載完畢。
  • DOMContentLoaded -> load。

5. 什麼是 CRP,即關鍵渲染路徑(Critical Rendering Path)? 如何優化 ?

關鍵渲染路徑是瀏覽器將 HTML CSS JavaScript 轉換爲在屏幕上呈現的像素內容所經歷的一系列步驟。也就是咱們上面說的瀏覽器渲染流程。

爲儘快完成首次渲染,咱們須要最大限度減少如下三種可變因素:

  • 關鍵資源的數量: 可能阻止網頁首次渲染的資源。
  • 關鍵路徑長度: 獲取全部關鍵資源所需的往返次數或總時間。
  • 關鍵字節: 實現網頁首次渲染所需的總字節數,等同於全部關鍵資源傳送文件大小的總和。

1. 優化 DOM

  • 刪除沒必要要的代碼和註釋包括空格,儘可能作到最小化文件。
  • 能夠利用 GZIP 壓縮文件。
  • 結合 HTTP 緩存文件。

2. 優化 CSSOM

縮小、壓縮以及緩存一樣重要,對於 CSSOM 咱們前面重點提過了它會阻止頁面呈現,所以咱們能夠從這方面考慮去優化。

  • 減小關鍵 CSS 元素數量
  • 當咱們聲明樣式表時,請密切關注媒體查詢的類型,它們極大地影響了 CRP 的性能 。

3. 優化 JavaScript

當瀏覽器遇到 script 標記時,會阻止解析器繼續操做,直到 CSSOM 構建完畢,JavaScript 纔會運行並繼續完成 DOM 構建過程。

  • async: 當咱們在 script 標記添加 async 屬性之後,瀏覽器遇到這個 script 標記時會繼續解析 DOM,同時腳本也不會被 CSSOM 阻止,即不會阻止 CRP。
  • defer: 與 async 的區別在於,腳本須要等到文檔解析後( DOMContentLoaded 事件前)執行,而 async 容許腳本在文檔解析時位於後臺運行(二者下載的過程不會阻塞 DOM,但執行會)。
  • 當咱們的腳本不會修改 DOM 或 CSSOM 時,推薦使用 async 。
  • 預加載 —— preload & prefetch 。
  • DNS 預解析 —— dns-prefetch 。

總結

  • 分析並用 關鍵資源數 關鍵字節數 關鍵路徑長度 來描述咱們的 CRP 。
  • 最小化關鍵資源數: 消除它們(內聯)、推遲它們的下載(defer)或者使它們異步解析(async)等等 。
  • 優化關鍵字節數(縮小、壓縮)來減小下載時間 。
  • 優化加載剩餘關鍵資源的順序: 讓關鍵資源(CSS)儘早下載以減小 CRP 長度 。

前端性能優化之關鍵路徑渲染優化

6. defer 和 async 的區別 ?

當瀏覽器碰到 script 腳本的時候 :

1. <script src="script.js">

沒有 defer 或 async,瀏覽器會當即加載並執行指定的腳本,「當即」指的是在渲染該 script 標籤之下的文檔元素以前,也就是說不等待後續載入的文檔元素,讀到就加載並執行。

2. <script async src="script.js">

有 async,加載和渲染後續文檔元素的過程將和 script.js 的加載與執行並行進行(異步)。

3. <script defer src="myscript.js">

有 defer,加載後續文檔元素的過程將和 script.js 的加載並行進行(異步),可是 script.js 的執行要在全部元素解析完成以後,DOMContentLoaded 事件觸發以前完成。

從實用角度來講,首先把全部腳本都丟到 </body> 以前是最佳實踐,由於對於舊瀏覽器來講這是惟一的優化選擇,此法可保證非腳本的其餘一切元素可以以最快的速度獲得加載和解析。

接着,咱們來看一張圖:

defer_async

defer_async

藍色線表明網絡讀取,紅色線表明執行時間,這倆都是針對腳本的。綠色線表明 HTML 解析。

所以,咱們能夠得出結論:

  1. defer 和 async 在網絡讀取(下載)這塊兒是同樣的,都是異步的(相較於 HTML 解析)
  2. 它倆的差異在於腳本下載完以後什麼時候執行,顯然 defer 是最接近咱們對於應用腳本加載和執行的要求的
  3. 關於 defer,此圖未盡之處在於它是按照加載順序執行腳本的,這一點要善加利用
  4. async 則是一個亂序執行的主,反正對它來講腳本的加載和執行是牢牢挨着的,因此無論你聲明的順序如何,只要它加載完了就會馬上執行
  5. 仔細想一想,async 對於應用腳本的用處不大,由於它徹底不考慮依賴(哪怕是最低級的順序執行),不過它對於那些能夠不依賴任何腳本或不被任何腳本依賴的腳原本說倒是很是合適的

來自 defer 和 async 的區別 -- nightire 回答

7. 談談瀏覽器的迴流與重繪

迴流必將引發重繪,重繪不必定會引發迴流。

迴流(Reflow)

當 Render Tree 中部分或所有元素的尺寸、結構、或某些屬性發生改變時,瀏覽器從新渲染部分或所有文檔的過程稱爲迴流。

會致使迴流的操做:

頁面首次渲染

瀏覽器窗口大小發生改變

元素尺寸或位置發生改變元素內容變化(文字數量或圖片大小等等)

元素字體大小變化

添加或者刪除可見的 DOM 元素

激活 CSS 僞類(例如::hover)

查詢某些屬性或調用某些方法

一些經常使用且會致使迴流的屬性和方法:

clientWidth、clientHeight、clientTop、clientLeft  
     
   offsetWidth、offsetHeight、offsetTop、offsetLeft  
     
   scrollWidth、scrollHeight、scrollTop、scrollLeft  
     
   scrollIntoView()、scrollIntoViewIfNeeded()  
     
   getComputedStyle()  
     
   getBoundingClientRect()  
     
   scrollTo()

重繪(Repaint)

當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color、background-color、visibility 等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。

性能影響

迴流比重繪的代價要更高。

有時即便僅僅迴流一個單一的元素,它的父元素以及任何跟隨它的元素也會產生迴流。現代瀏覽器會對頻繁的迴流或重繪操做進行優化:瀏覽器會維護一個隊列,把全部引發迴流和重繪的操做放入隊列中,若是隊列中的任務數量或者時間間隔達到一個閾值的,瀏覽器就會將隊列清空,進行一次批處理,這樣能夠把屢次迴流和重繪變成一次。

當你訪問如下屬性或方法時,瀏覽器會馬上清空隊列:

clientWidth、clientHeight、clientTop、clientLeft  
      
      
    offsetWidth、offsetHeight、offsetTop、offsetLeft  
      
      
    scrollWidth、scrollHeight、scrollTop、scrollLeft  
      
      
    width、height  
      
      
    getComputedStyle()  
      
      
    getBoundingClientRect()

由於隊列中可能會有影響到這些屬性或方法返回值的操做,即便你但願獲取的信息與隊列中操做引起的改變無關,瀏覽器也會強行清空隊列,確保你拿到的值是最精確的。

如何避免

CSS
  • 避免使用 table 佈局。
  • 儘量在 DOM 樹的最末端改變 class。
  • 避免設置多層內聯樣式。
  • 將動畫效果應用到 position 屬性爲 absolute 或 fixed 的元素上。
  • 避免使用 CSS 表達式(例如:calc())。
Javascript
  • 避免頻繁操做樣式,最好一次性重寫 style 屬性,或者將樣式列表定義爲 class 並一次性更改 class 屬性。
  • 避免頻繁操做 DOM,建立一個 documentFragment,在它上面應用全部 DOM 操做,最後再把它添加到文檔中。
  • 也能夠先爲元素設置 display: none,操做結束後再把它顯示出來。由於在 display 屬性爲 none 的元素上進行的 DOM 操做不會引起迴流和重繪。
  • 避免頻繁讀取會引起迴流/重繪的屬性,若是確實須要屢次使用,就用一個變量緩存起來。
  • 對具備複雜動畫的元素使用絕對定位,使它脫離文檔流,不然會引發父元素及後續元素頻繁迴流。

8. 什麼是渲染層合併 (Composite) ?

渲染層合併,對於頁面中 DOM 元素的繪製(Paint)是在多個層上進行的。

在每一個層上完成繪製過程以後,瀏覽器會將繪製的位圖發送給 GPU 繪製到屏幕上,將全部層按照合理的順序合併成一個圖層,而後在屏幕上呈現。

對於有位置重疊的元素的頁面,這個過程尤爲重要,由於一旦圖層的合併順序出錯,將會致使元素顯示異常。

composite

composite

RenderLayers 渲染層,這是負責對應 DOM 子樹。

GraphicsLayers 圖形層,這是負責對應 RenderLayers 子樹。

RenderObjects 保持了樹結構,一個 RenderObjects 知道如何繪製一個 node 的內容, 他經過向一個繪圖上下文(GraphicsContext)發出必要的繪製調用來繪製 nodes。

每一個 GraphicsLayer 都有一個 GraphicsContext,GraphicsContext 會負責輸出該層的位圖,位圖是存儲在共享內存中,做爲紋理上傳到 GPU 中,最後由 GPU 將多個位圖進行合成,而後 draw 到屏幕上,此時,咱們的頁面也就展示到了屏幕上。

GraphicsContext 繪圖上下文的責任就是向屏幕進行像素繪製(這個過程是先把像素級的數據寫入位圖中,而後再顯示到顯示器),在 chrome 裏,繪圖上下文是包裹了的 Skia(chrome 本身的 2d 圖形繪製庫)

某些特殊的渲染層會被認爲是合成層(Compositing Layers),合成層擁有單獨的 GraphicsLayer,而其餘不是合成層的渲染層,則和其第一個擁有 GraphicsLayer 父層公用一個。

合成層的優勢

一旦 renderLayer 提高爲了合成層就會有本身的繪圖上下文,而且會開啓硬件加速,有利於性能提高。

  • 合成層的位圖,會交由 GPU 合成,比 CPU 處理要快 (提高到合成層後合成層的位圖會交 GPU 處理,但請注意,僅僅只是合成的處理(把繪圖上下文的位圖輸出進行組合)須要用到 GPU,生成合成層的位圖處理(繪圖上下文的工做)是須要 CPU。)
  • 當須要 repaint 時,只須要 repaint 自己,不會影響到其餘的層 (當須要 repaint 的時候能夠只 repaint 自己,不影響其餘層,可是 paint 以前還有 style, layout,那就意味着即便合成層只是 repaint 了本身,但 style 和 layout 自己就很佔用時間。)
  • 對於 transform 和 opacity 效果,不會觸發 layout 和 paint (僅僅是 transform 和 opacity 不會引起 layout 和 paint,其餘的屬性不肯定。)

通常一個元素開啓硬件加速後會變成合成層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能。

注意不能濫用 GPU 加速,必定要分析其實際性能表現。由於 GPU 加速建立渲染層是有代價的,每建立一個新的渲染層,就意味着新的內存分配和更復雜的層的管理。而且在移動端 GPU 和 CPU 的帶寬有限制,建立的渲染層過多時,合成也會消耗跟多的時間,隨之而來的就是耗電更多,內存佔用更多。過多的渲染層來帶的開銷而對頁面渲染性能產生的影響,甚至遠遠超過了它在性能改善上帶來的好處。

這裏就不細說了,有興趣的童鞋推薦如下三篇文章 ~

Accelerated Rendering in Chrome

CSS GPU Animation: Doing It Right

無線性能優化:Composite

參考

史上最全!圖解瀏覽器的工做原理

從瀏覽器多進程到 JS 單線程,JS 運行機制最全面的一次梳理

後記

若是你和我同樣喜歡前端,也愛動手摺騰,歡迎關注我一塊兒玩耍啊~ ❤️

github 地址,歡迎 follow 哦~

博客

個人博客,點 star,不迷路~

公衆號

前端時刻

前端時刻

前端時刻

相關文章
相關標籤/搜索