從內部來看瀏覽器到底在做什麼?(Inside look at modern web browser)

瀏覽器背後是怎麼運作的?(Multi-process architecture)

這部份主要是來談支持瀏覽器運作的背後機制。

CPU 就像是人類的腦,並且由於多核心可並行處理許多任務;而 GPU 大多用於處理跨核心的工作與 3D 圖形繪製相關的任務,記憶體則用來儲存各個 process 的狀態(像是要用到的資料等),而每當啟動一個應用程式時,就會啟用至少一個 process,每個 process 又可以切分(或說分配會比較恰當)許多 thread 來協助其運作,而 process 與 process 間利用 IPC(inter process communication)來溝通。

Chrome 是 multi-process 架構的,也就是多個 process 聯合運作以完成任務,因此,對於啟動一個瀏覽器應用程式來說,會啟動到許多的 process,例如:browser process、renderer process、plugin process 與 GPU process…browser process 主控瀏覽器相關,像是書籤功能、發出網路請求等;renderer process 用於頁面顯示;plugin process 用於控制網站所用的 plugin,例如:flash;GPU process 用於處理跨核心的工作與 3D 圖形繪製相關等。並且,每個 tab 擁有自己獨立運作的(renderer)process。

不同的 process 可切分為不同的 thread 以負責不同的功能

multi-process 架構的優點是其一 process 若故障無反應,並不會對其他功能造成影響,例如:瀏覽器頁籤的其中一個 process 掛了,也只是那個頁籤無法正常顯示而已,其他 tab 依然持續運作,使用者仍可繼續瀏覽、互動。也由於每個 process 有配置其獨立的記憶體空間,因此除了必要的資源外並不共享,安全性較高。

目前的 Chrome 可做 site isolation,意即每個 iframe 也是由獨立運作的(renderer)process 來負責執行,而這些 process 又有分配獨立的記憶體空間,對於安全性來說,隔離不同網站的記憶體使用的資訊,可說是同源策略(same-origin policy)的第二層保護機制。

從使用者輸入網址到網頁渲染完畢,到底發生了什麼事情?(Navigation flow)

承上,經由許多的 process 與 thread 合作才能渲染成功一個畫面,那麼,到底經歷了什麼事情呢?

當使用者在瀏覽器的網址列輸入網址並按下 enter 後,browser process 的 ui thread 接收到使用者輸入的資料,ui thread 會發出網路請求,以期獲得展示網頁所需的資料,並且此時 network thread 開始執行 DNS lookup 等工作,以發出真正的網路請求。待 network thread 收到回應(response body / payload)後,先經由 contetn-type header 判別是何種資料(若無 contetn-type header 則做 MIME type sniffing),並做安全性和 CORB 檢查,確認是安全的資料後,network thread 才會通知 ui thread 資料已準備完畢,轉交給 renderer process 來接手後續網頁渲染的工作。

browser process 經 IPC 通知 renderer process 進入渲染的工作的同時,ui thread 也會更新瀏覽器的 UI,例如:更新網址列以符合目前要載入網站的位置。待渲染完成,renderer process 再度 IPC browser process 其工作結束,觸發 onload 事件,並且 JavaScript 仍持續載入相關資源並更新畫面。

如果此時在網址列輸入新網址而想要轉跳到另一個頁面呢?除了仍會經歷以上的步驟外,還會確定目前渲染的網站是否需要處理 beforeunload 事件,這個 beforeunload 事件會在瀏覽器關閉或刷新時觸發,並發出「Leave this site?」的提示訊息。

當要前往到新的頁面時,browser process 會 IPC renderer process 是否要處理 beforeunload 事件,同時也呼叫另一個 renderer process 來處理新頁面的渲染工作。

Service Wroker

service worker(在此簡稱 sw)可將資料快取在本地,並掌握檔案更新的時機。sw 會在 renderer process 中運行,但 browser process 要如何知道 sw 的存在待資料取得後轉交給 renderer process 呢?在 ui thread 發出網路請求並請 network thread 處理後續工作時,network thread 會先到已註冊的 sw scope 確認是否有想要的檔案,若有就將這個檔案交由 renderer process 來處理。

另外,「navigation preload」是一種讓 sw 平行下載其所需資料的加速機制,在 sw 被啟動的同時,network thread 會發出網路請求來下載 sw 所要求的資源,而非待 sw 要求後才下載檔案。

Renderer Process 做了什麼事情?

renderer process 的主要工作是將程式碼轉為頁面以呈現給使用者。開發者所撰寫的程式碼多由 main thread 處理,但若使用 web worker 或 service worker 則由 worker thread 來處理,另外,compoisitor thread 與 raster thread 則負責處理畫面渲染的工作。

renderer process 的 main thread 會剖析(parse)HTML 字串並產出 DOM tree,main thread 在剖析 HTML 時,若發現需下載的資源,就會透過 IPC 通知 browser process 的 network thread 來發出網路請求以取得該資源;或使用加速的 preload scanner 方法,意即在剖析 HTML 的同時,即掃描是否有需要下載的資源,若有就透過 IPC 通知 browser process 的 network thread 來發出網路請求以取得該資源,剖析與下載檔案平行進行。

而當下載到 JavaScript 檔案時可能會阻礙 HTML 的剖析,這是因為 HTML parser 遇到 <script> 標籤就會停下來下載、解析和執行。若 HTML 的剖析不想被阻礙,解法是使用 async 或 defer。

async, defer

圖片來源:Loading Third-Party JavaScript

也可使用以下方法讓 JavaScript 檔案的下載方式能處理得更好

檔案下載完畢後,browser process 便會交於 renderer process 來處理,在樣式計算階段,renderer process 的 main thread 會剖析 CSS 並計算 DOM 節點的樣式並產出 render tree,以供 layout 階段分配元素位置。render tree 很類似 DOM tree,但只包含可見元素。在 paint 階段,main thread 繪製畫面並調整 layer tree 來處理圖層順序,這階段成本很高,應盡量避免,否則很容易產生 jank 的狀況。解法是使用 requestAnimationFrame 或 web worker 來減輕 main thread 的負擔-requestAnimationFrame 讓 main thread 決定處理事情的時機,而 web worker 可將工作轉移到 worker thread,避免 main thread 因過多任務而阻塞。在 compositor 階段會由 compositor thread 層層輸出樣式,在此可使用 will-change 來作升階的動作。

為什麼只更動 compositor 階段的動畫是效能最佳的呢?這是因為 compositor thread 不需 main thread 來處理樣式計算、繪製和 JavaScript 執行的部份,因此效能佳、動畫順暢。效能相關可參考這裡

如何減輕合成器的負擔,讓使用者與畫面的互動更為流暢?

當使用者與頁面互動(例如:click、touch、scroll)時,browser process 的 ui thread 接收到這個動作並經由 IPC 通知 renderer process 的 main thread 找到事件目標與執行對應的事件監聽器。

當頁面渲染合成後,若某個區域有綁定事件監聽器,則這個區域會被標記為「non-fase scrollable region」,意即 compositor thread 會確保此區域的事件發生時會送 input event 給 main thread。而若事件發生在區域外,compositor thread 就不會跟 main thread 溝通而會立即進行合成的動作。備註,當 compositor thread 送 input event 給 main thread 時所給定的位置,是利用繪製(paint)階段而得到的座標值。

順道一提,我們常用 event delegation 對一大塊區域的元素進行事件綁定,這樣的效能並不好,原因是這個區域有些元素可能是不需要被監聽的,但仍會與 main thread 進行溝通,解法是加上 passive: true 提示 browser process 讓合成器可先進行合成而不需等待 main thread 回應。

在最小化傳送到 main thread 的事件派遣的數量上,為的是免過度與 main thread 溝通而執行 JavaScript 程式碼導致 jank 的不流暢問題,解法是 Chrome 會合併連續的事件(例如:wheel、mousewheel、mousemove、pointermove、touchmove),並可利用 requestAnimationFrame 來延遲派遣。再來,getCoalescedEvents 可獲得幀內的事件而能處理更複雜細膩的圖形繪製等工作。

若不知如何著手效能改善,可利用工具 Lighthout 進行效能檢測並查看其建議,或查看 feature policy 根據需求加入建議以實作更好的網站。

備註

References


效能調校 轉譯效能 關鍵轉譯路徑 Rendering Performance Critical Rendering Path Lighthouse Resource Hints Web Workers Worker requestAnimationFrame 前端效能 系列文