移動互聯網時代,用戶對於網頁的打開速度要求愈來愈高。百度用戶體驗部研究代表,頁面放棄率和頁面的打開時間關係以下圖 所示。css
charthtml
根據百度用戶體驗部的研究結果來看,普通用戶指望且可以接受的頁面加載時間在 3 秒之內。若頁面的加載時間過慢,用戶就會失去耐心而選擇離開。前端
首屏做爲直面用戶的第一屏,其重要性不言而喻。優化用戶體驗更是咱們前端開發很是須要 focus 的東西之一。html5
本文咱們經過 8 道面試題來聊聊瀏覽器渲染過程與性能優化。node
咱們首先帶着這 8 個問題,來了解瀏覽器渲染過程,後面會給出題解~git
進程(process)和線程(thread)是操做系統的基本概念。github
進程是 CPU 資源分配的最小單位(是能擁有資源和獨立運行的最小單位)。web
線程是 CPU 調度的最小單位(是創建在進程基礎上的一次程序運行單位)。面試
process_threadchrome
現代操做系統都是能夠同時運行多個任務的,好比:用瀏覽器上網的同時還能夠聽音樂。
對於操做系統來講,一個任務就是一個進程,好比打開一個瀏覽器就是啓動了一個瀏覽器進程,打開一個 Word 就啓動了一個 Word 進程。
有些進程同時不止作一件事,好比 Word,它同時能夠進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時作多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程。
因爲每一個進程至少要作一件事,因此一個進程至少有一個線程。系統會給每一個進程分配獨立的內存,所以進程有它獨立的資源。同一進程內的各個線程之間共享該進程的內存空間(包括代碼段,數據集,堆等)。
借用一個生動的比喻來講,進程就像是一個有邊界的生產廠間,而線程就像是廠間內的一個個員工,能夠本身作本身的事情,也能夠相互配合作同一件事情。
當咱們啓動一個應用,計算機會建立一個進程,操做系統會爲進程分配一部份內存,應用的全部狀態都會保存在這塊內存中。
應用也許還會建立多個線程來輔助工做,這些線程能夠共享這部份內存中的數據。若是應用關閉,進程會被終結,操做系統會釋放相關內存。
process_thread_example
一個好的程序經常被劃分爲幾個相互獨立又彼此配合的模塊,瀏覽器也是如此。
以 Chrome 爲例,它由多個進程組成,每一個進程都有本身核心的職責,它們相互配合完成瀏覽器的總體功能,
每一個進程中又包含多個線程,一個進程內的多個線程也會協同工做,配合完成所在進程的職責。
Chrome 採用多進程架構,其頂層存在一個 Browser process 用以協調瀏覽器的其它進程。
process
因爲默認 新開 一個 tab 頁面 新建 一個進程,因此單個 tab 頁面崩潰不會影響到整個瀏覽器。
一樣,第三方插件崩潰也不會影響到整個瀏覽器。
多進程能夠充分利用現代 CPU 多核的優點。
方便使用沙盒模型隔離插件等進程,提升瀏覽器的穩定性。
系統爲瀏覽器新開的進程分配內存、CPU 等資源,因此內存和 CPU 的資源消耗也會更大。
不過 Chrome 在內存釋放方面作的不錯,基本內存都是能很快釋放掉給其餘程序運行的。
process_list
負責瀏覽器界面的顯示與交互。各個頁面的管理,建立和銷燬其餘進程。網絡的資源管理、下載等。
每種類型的插件對應一個進程,僅當使用該插件時才建立。
最多隻有一個,用於 3D 繪製等
稱爲瀏覽器渲染進程或瀏覽器內核,內部是多線程的。主要負責頁面渲染,腳本執行,事件處理等。 (本文重點分析)
瀏覽器的渲染進程是多線程的,咱們來看看它有哪些主要線程 :
renderder_process
若是要講從輸入 url 到頁面加載發生了什麼,那怕是沒完沒了了…這裏咱們只談談瀏覽器渲染的流程。
workflow
這是由於 Javascript 這門腳本語言誕生的使命所致!JavaScript 爲處理頁面中用戶的交互,以及操做 DOM 樹、CSS 樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。
若是 JavaScript 是多線程的方式來操做這些 UI DOM,則可能出現 UI 操做的衝突。
若是 Javascript 是多線程的話,在多線程的交互下,處於 UI 中的 DOM 節點就可能成爲一個臨界資源,
假設存在兩個線程同時操做一個 DOM,一個負責修改一個負責刪除,那麼這個時候就須要瀏覽器來裁決如何生效哪一個線程的執行結果。
固然咱們能夠經過鎖來解決上面的問題。但爲了不由於引入了鎖而帶來更大的複雜性,Javascript 在最初就選擇了單線程執行。
因爲 JavaScript 是可操縱 DOM 的,若是在修改這些元素屬性同時渲染界面(即 JavaScript 線程和 UI 線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。
所以爲了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JavaScript 引擎爲互斥的關係。
當 JavaScript 引擎執行時 GUI 線程會被掛起,GUI 更新會被保存在一個隊列中等到引擎線程空閒時當即被執行。
從上面咱們能夠推理出,因爲 GUI 渲染線程與 JavaScript 執行線程是互斥的關係,
當瀏覽器在執行 JavaScript 程序的時候,GUI 渲染線程會被保存在一個隊列中,直到 JS 程序執行完成,纔會接着執行。
所以若是 JS 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。
由上面瀏覽器渲染流程咱們能夠看出 :
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 的執行。
關鍵渲染路徑是瀏覽器將 HTML CSS JavaScript 轉換爲在屏幕上呈現的像素內容所經歷的一系列步驟。也就是咱們上面說的瀏覽器渲染流程。
爲儘快完成首次渲染,咱們須要最大限度減少如下三種可變因素:
縮小、壓縮以及緩存一樣重要,對於 CSSOM 咱們前面重點提過了它會阻止頁面呈現,所以咱們能夠從這方面考慮去優化。
當瀏覽器遇到 script 標記時,會阻止解析器繼續操做,直到 CSSOM 構建完畢,JavaScript 纔會運行並繼續完成 DOM 構建過程。
當瀏覽器碰到 script 腳本的時候 :
沒有 defer 或 async,瀏覽器會當即加載並執行指定的腳本,「當即」指的是在渲染該 script 標籤之下的文檔元素以前,也就是說不等待後續載入的文檔元素,讀到就加載並執行。
有 async,加載和渲染後續文檔元素的過程將和 script.js 的加載與執行並行進行(異步)。
有 defer,加載後續文檔元素的過程將和 script.js 的加載並行進行(異步),可是 script.js 的執行要在全部元素解析完成以後,DOMContentLoaded 事件觸發以前完成。
從實用角度來講,首先把全部腳本都丟到 </body> 以前是最佳實踐,由於對於舊瀏覽器來講這是惟一的優化選擇,此法可保證非腳本的其餘一切元素可以以最快的速度獲得加載和解析。
接着,咱們來看一張圖:
defer_async
藍色線表明網絡讀取,紅色線表明執行時間,這倆都是針對腳本的。綠色線表明 HTML 解析。
所以,咱們能夠得出結論:
來自 defer 和 async 的區別 -- nightire 回答
迴流必將引發重繪,重繪不必定會引發迴流。
當 Render Tree 中部分或所有元素的尺寸、結構、或某些屬性發生改變時,瀏覽器從新渲染部分或所有文檔的過程稱爲迴流。
會致使迴流的操做:
頁面首次渲染
瀏覽器窗口大小發生改變
元素尺寸或位置發生改變元素內容變化(文字數量或圖片大小等等)
元素字體大小變化
添加或者刪除可見的 DOM 元素
激活 CSS 僞類(例如::hover)
查詢某些屬性或調用某些方法
一些經常使用且會致使迴流的屬性和方法:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft scrollIntoView()、scrollIntoViewIfNeeded() getComputedStyle() getBoundingClientRect() scrollTo()
當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color、background-color、visibility 等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。
迴流比重繪的代價要更高。
有時即便僅僅迴流一個單一的元素,它的父元素以及任何跟隨它的元素也會產生迴流。現代瀏覽器會對頻繁的迴流或重繪操做進行優化:瀏覽器會維護一個隊列,把全部引發迴流和重繪的操做放入隊列中,若是隊列中的任務數量或者時間間隔達到一個閾值的,瀏覽器就會將隊列清空,進行一次批處理,這樣能夠把屢次迴流和重繪變成一次。
當你訪問如下屬性或方法時,瀏覽器會馬上清空隊列:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft width、height getComputedStyle() getBoundingClientRect()
由於隊列中可能會有影響到這些屬性或方法返回值的操做,即便你但願獲取的信息與隊列中操做引起的改變無關,瀏覽器也會強行清空隊列,確保你拿到的值是最精確的。
渲染層合併,對於頁面中 DOM 元素的繪製(Paint)是在多個層上進行的。
在每一個層上完成繪製過程以後,瀏覽器會將繪製的位圖發送給 GPU 繪製到屏幕上,將全部層按照合理的順序合併成一個圖層,而後在屏幕上呈現。
對於有位置重疊的元素的頁面,這個過程尤爲重要,由於一旦圖層的合併順序出錯,將會致使元素顯示異常。
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 加速,必定要分析其實際性能表現。由於 GPU 加速建立渲染層是有代價的,每建立一個新的渲染層,就意味着新的內存分配和更復雜的層的管理。而且在移動端 GPU 和 CPU 的帶寬有限制,建立的渲染層過多時,合成也會消耗跟多的時間,隨之而來的就是耗電更多,內存佔用更多。過多的渲染層來帶的開銷而對頁面渲染性能產生的影響,甚至遠遠超過了它在性能改善上帶來的好處。
這裏就不細說了,有興趣的童鞋推薦如下三篇文章 ~
Accelerated Rendering in Chrome
CSS GPU Animation: Doing It Right
從瀏覽器多進程到 JS 單線程,JS 運行機制最全面的一次梳理
若是你和我同樣喜歡前端,也愛動手摺騰,歡迎關注我一塊兒玩耍啊~ ❤️
前端時刻
前端時刻