網站性能優化實戰(二)

轉自IMWeb社區,做者:jerryOnlyZRJ,原文連接前端

——從Webkit內部渲染機制出發,談網站渲染性能優化

本文是對前文:imweb.io/topic/5b6fd… 相關知識的補充,文中的「前文」一詞同此。git

特以此文向《WebKit技術內幕》做者朱永盛前輩致敬。github

0.引言

自上次發佈了《網站性能優化實戰——從12.67s到1.06s的故事》一文後,發現本身對頁面渲染性能這個版塊介紹的內容還不夠完善,爲了更清晰的梳理瀏覽器渲染頁面的機制,以讓讀者更爲全面瞭解渲染性能優化的深層次原理,筆者在課餘時間從新研讀了一遍《WebKit技術內幕》一書,將本身的總結經驗分享予論壇同僚。文章更新可能以後不會實時同步在論壇上,歡迎你們關注個人Github,我會把最新的文章更新在對應的項目裏,讓咱們一塊兒在代碼的海洋裏策馬奔騰:github.com/jerryOnlyZR…web

讓咱們用本身的雙手,創造出極致的頁面渲染性能。面試

由於本文是基於前文的基礎上拓展了相關內容,因此可能會有部分文字重複,但願你們不要介意。算法

1.瀏覽器內核

仍是獻上前文的那張瀏覽器渲染引擎、HTML解釋器、JS解釋器關係圖:chrome

咱們平時打開瀏覽器所看到的界面,就是圖裏的User Interface,咱們常說的瀏覽器內核,指的就是咱們的渲染引擎——Rendering engine,最著名的還屬Chrome的前任、Safari的搭檔WebKit,咱們使用的大多數移動端產品(Android、IOS 等等)都是使用的它,也就是說咱們能夠在手機上實現咱們的CSS動畫、JS交互等等效果,這也是咱們的前端開發人員可以開發出Web和Hybrid APP的緣由,包括如今的Blink,其實也應該算是Webkit的一個變種,它是從WebKit衍生來的,可是Google在和WebKit分手後便在Blink裏使用了聲名遠播的V8引擎,打出了一場漂亮的翻身戰。還有IE的Trident,火狐的Gecko瀏覽器內核,平時咱們須要爲部分CSS樣式添加兼容性前綴,正是由於不一樣的瀏覽器使用了不一樣的渲染引擎,產生了不一樣的渲染機制。npm

渲染引擎內包括了咱們的HTML解釋器,CSS樣式解釋器和JS解釋器,不過如今咱們會經常聽到人們說V8引擎,咱們常常接觸的Node.js也是用的它,這是由於JS的做用愈來愈重要,工做愈來愈繁雜,因此JS解釋器也漸漸獨立出來,成爲了單獨的JS引擎。編程

2.瀏覽器架構

在你深刻探知瀏覽器內部機理以前,你必須知道,瀏覽器是多進程、多線程模型,這裏咱們以基於Blink內核的Chromium瀏覽器爲例,講講在Chromium瀏覽器中,幾個常見的進程:瀏覽器

  • Browser進程:這是瀏覽器的主進程,負責瀏覽器界面的顯示、各個頁面的管理。每次咱們打開瀏覽器,都會啓動一個Browser進程,結束該進程就會關閉咱們的瀏覽器。
  • Renderer進程:這是網頁的渲染進程,負責頁面的渲染工做,通常來講,一個頁面都會對應一個Renderer進程,不過也有例外。
  • GPU進程:若是頁面啓動了硬件加速,瀏覽器就會開啓一個GPU進程,可是最多隻能有一個,當且僅當GPU硬件加速打開的時候纔會被建立。

剛剛咱們提到的全部進程,他們都具備以下特徵:

  1. Browser進程和頁面的渲染是分開的,這保證了頁面渲染致使的崩潰不會致使瀏覽器主界面的崩潰。
  2. 每一個頁面都是獨立的進程,這樣就保證了頁面之間不會相互影響。
  3. GPU進程也是獨立的。

爲了能讓你們更爲直觀的理解Chromium多進程模型,筆者附上一張Chrome瀏覽器在Windows上的多進程示例圖:(打開任務管理器,將進程按照「命令行」排序,找到「Google Chrome」相關內容)

從進程的type參數中,咱們能夠區分出不一樣類型的進程,而那個不帶type參數的進程,指的就是咱們的Browser瀏覽器主進程。

每一個進程的內部,都有不少的線程,多線程的主要目的就是爲了保持用戶界面的高響應度,保證UI線程(Browser進程中的主線程)不會被被其餘費事的操做阻礙從而影響了對用戶操做的響應。就像咱們平時所說的JS腳本解釋執行,都是在獨立的線程中的,這也是JS這門編程語言特立獨行的地方,它是單線程腳本。

在這裏作一些簡單的拓展,你們看看下面這段代碼:

setTimeout(function(){
  console.log("我能被輸出嗎?")
}, 0)
while(true){
  var a = 1;
}
複製代碼

你們確定都知道由於線程阻塞,定時器裏的console並不會被輸出,這就是由於咱們的JS解釋執行是單線程的,因此在執行過程當中須要將同步和異步的兩類代碼分別壓入同步堆棧和異步隊列中,經過Event Loop實現異步操做:

也就說咱們的JS定時器其實並非徹底準確的,還須要考慮同步堆棧中代碼執行產生的延遲。

不過如今有不少技術可讓咱們的JS代碼模擬多線程執行,包括以前一位日本大牛編寫的一個名爲Concurrent.Thread.js 的插件,還有HTML5標準中提出的Web Worker,這些工具都能讓咱們實現多線程執行JS代碼的效果。

3.HTML網頁結構及渲染機制淺析

在瞭解瀏覽器渲染機制以前你必須理解瀏覽器的層級結構,或許你知道瀏覽器的渲染頻率是60fps,知道瀏覽器的頁面呈現就如同電影般是逐幀渲染的效果,但並不表明頁面就像膠片同樣,從頭到尾都是單層的。頁面所經歷的,是從一個像千層麪同樣的東西一步步合成的過程,中間經歷了軟硬件渲染等等過程,最後造成一個完整的合成層才被渲染出來。千層麪的效果大概就像Firefox的3D View插件所呈現出的那般:

有人可能會說刨得這麼深咱們實際開發中用獲得嗎?若是我這麼和你說「性能優化不是講究減小重排重繪嘛,我如今手上有一套方案,能讓你的頁面動畫直接跳太重排重繪的環節」,你是否會對此產生一點興趣?不過不着急,在咱們尚未把其中原理理清以前,我是不會草率地放出解決方案的,否則很容易就會讓你們的思想偏離正軌,由於我就是經歷了那樣一個慘痛的過程過來的。

若是要驗證我上述所言非憑空捏造,你們能夠打開chrome開發者工具中的performance版塊,錄製一小段頁面渲染,並將輸出結果切換至Event Log版塊,你們就能夠清晰地看見網站渲染經歷的過程:

在Activity字段中咱們能夠看到,咱們的頁面經歷了從新計算樣式→更新Layer樹→繪製→合成合成層的過程,結合咱們的Summary版塊中的環形圖,咱們能夠大體把頁面渲染分爲三個階段:

  • 第一階段,資源加載及腳本執行階段:在Summary圖中咱們能夠看到,頁面在渲染時藍色的Loading(資源請求)部分和黃色的Scripting(腳本執行)部分佔用了大量的時間,多是由於咱們請求的資源體積較多,執行的腳本複雜度較大,咱們能夠依據網絡傳輸性能優化的相關內容對這一階段進行優化。
  • 第二階段,頁面佈局階段:在Summary圖中,紫色的Rendering部分指的就是咱們的layout頁面佈局階段,在Event Log版塊之因此沒有看到layout activity,是由於我啓動了硬件加速,使得頁面在從新渲染時不會發生重排,可能對這句話你如今還聽的雲裏霧裏,等你看完這篇文章,你就會明白其中道理的。之因此把layout單獨提取出來,是由於它是一個很特別的過程,它會影響RenderLayers的生成,也會大量消耗CPU資源。
  • 第三階段,頁面繪製階段:其中就包括了最後的Painting和Composite Layers的全部過程。

4.DOM樹及事件機制

若是你學過計算機網絡,或者數字電子技術,那麼你必定知道,資源在網路中傳輸的形式是字節流。咱們每次請求一個頁面,都通過了字節流→HTML文檔→DOM Tree的過程,其中細節我已在前一篇文章中的navigation timing版塊做了詳細介紹,今天咱們只談DOM樹構建以後瀏覽器的相關工做。

DOM樹的根是document,也就是咱們常常在瀏覽器審查元素時能看到的HTMLDocument,HTML文檔中的一個個標籤也被轉化成了一個個元素節點。

既然說到了DOM樹,就不得不說起瀏覽器的事件處理機制。事件處理最重要的兩個部分即是事件捕獲(Event Capture)和事件冒泡(Event Bubbling)。事件捕獲是自頂向下的,也就是說事件是從document節點發起,而後一路到達目標節點,反之,事件冒泡的過程則是自下而上的順序。

咱們常使用addEventListener() 方法來監聽事件,它包含三個參數,前兩個你們都太熟悉,咱們來聊聊第三個參數,MDN上將它稱做useCapture,類型爲Boolean。它的取值顯而易見,即是true和false(默認),若是設置爲true,表示在捕獲階段執行回調,而false則是在冒泡階段執行,它決定了父子節點的事件綁定函數的執行順序。

5.RenderObject和RenderLayer的構建

在DOM樹之中,某些節點是用戶不可見的,也就是說這些只是起一些其餘方面而不是顯示內容的做用。例如head節點、script節點,咱們能夠稱之爲「非可視化節點」。而另外的節點就是用來展現頁面內容的,包括咱們的body節點、div節點等等。對於這些「可視節點」,由於WebKit須要將它們的內容渲染到最終的頁面呈現中,因此WebKit會爲他們創建相應的RenderObject對象。一個RenderObject對象保存了爲了繪製DOM節點所須要的各類信息,其中包括樣式佈局信息等等。

可是構建的過程並無就此結束了,由於WebKit要對每個可視節點都生成一個RenderObject對象,若是當即對全部的對象進行渲染,假設咱們的頁面有上百個可視化元素,那將會是多麼複雜的一項工程啊。爲了減少網頁結構的複雜程度,並在不少狀況下可以減小從新渲染的開銷,WebKit會依據節點的樣式爲網頁的層次建立響應的RenderLayer對象。

當某些類型的RenderObject節點或者具備某些CSS樣式的RenderObject節點出現的時候,WebKit就會爲這些節點建立RenderLayer對象。RenderLayer節點和RenderObject節點不是一一對應關係,而是一對多的關係,其中生成RenderLayer的基本規則主要包括:

  • DOM樹的Document節點對應的RenderView節點
  • DOM樹中Document節點的子女節點,也就是HTML節點對應的RenderBlock節點
  • 顯式指定CSS位置的節點(position爲absolute或者fixed)
  • 具備透明效果的節點
  • 具備CSS 3D屬性的節點
  • 使用Canvas元素或者Video元素的節點

RenderLayer節點的使用能夠有效地減少網頁結構的複雜程度,並在不少狀況下可以減少從新渲染的開銷。通過梳理,RenderObject和RenderLayer的構建大概就是下圖這樣一個過程:

最後的構建結果將會以具體代碼的形式在WebKit中存儲起來:

「layer at (x, x)」表示的是不一樣的RenderLayer節點,下面的全部的RenderObject對象均屬於該RenderLayer對象。

這一板塊的內容你們只須要了解就好,有興趣能夠深究。

6.瀏覽器渲染方式

瀏覽器的渲染方式,主要分爲兩種,第一種是軟件渲染,第二種是硬件渲染。若是繪製工做只是由CPU完成,那麼稱之爲軟件渲染,若是繪製工做由GPU完成,則稱之爲硬件渲染。軟件渲染與硬件渲染有不一樣的緩存機制,只要咱們合理利用,就能發揮出最好的效果。

在軟件渲染中,一般的結果就是一個位圖(Bitmap)。若是在頁面的某一元素髮生了更新,WebKit只是首先計算須要更新的區域,而後只繪製同這些區域有交集的RenderObject節點。也就是說,若是更新區域跟某個Render-Layer節點有交集,WebKit就會繼續查找RenderLayer樹中包含的RenderObject子樹中的特定的一個或一些節點(這話好拗口,說的我都喘不過氣了),而不是去從新繪製整個RenderLayer對應的RenderObject子樹。以上內容,咱們也能夠稱之爲CPU緩存機制。

而硬件渲染的相關內容,咱們將在下一模塊以一個單獨的模塊進行介紹,由於相關的理論和優化的知識太多了。

7.深刻淺出硬件渲染

終於到了咱們的重頭戲了,若是你能參透硬件渲染機制並物盡其用,那麼基本上能夠說你在瀏覽器渲染性能上的造詣已經快登峯造極了。咱們剛剛已經說過,瀏覽器還有一種名爲硬件渲染的渲染方式,它是使用GPU的硬件能力來幫助渲染頁面。那麼,硬件渲染又是怎樣的一個過程呢?

WebKit會依據指定條件決定將那些RenderLayer對象組合在一塊兒造成一個新層並緩存在GPU,這一新層不久後會用於以後的合成,這些新層咱們統稱爲合成層(Compositing Layer)。對於一個RenderLayer對象,若是他不會造成一個合成層,那麼就會使用它的父親所使用的合成層,最後追溯到document。最後,由合成器(Compositor)將全部的合成層合成起來,造成網頁的最終可視化結果,實際上同軟件渲染的位圖同樣,也是一張圖片。

同觸發RenderLayer條件類似,知足必定條件或CSS樣式的RenderLayer會生成一個合成層:

  • 根節點document,由於全部不會生成合成層的RenderLayer最終都會追溯到它
  • RenderLayer具備CSS 3D屬性或者CSS透視效果(設置了translateZ()或者backface-visibility爲hidden)
  • RenderLayer包含的RenderObject節點表示的是使用硬件加速的HTML5 Video或者Canvas元素。
  • RenderLayer使用了基於animation或者transition的帶有CSS透明效果(opacity)或者CSS變換(transform)的動畫
  • RenderLayer有一個Z座標比本身小的兄弟節點,且該節點是一個合成層(在瀏覽器中的造成緣由Compositing Reason會提示:Compositing due to association with a element thay may overlap other composited elements ,意思就是你這個RenderLayer蓋在別的合成層至上啦,因此我瀏覽器要把你強制變成一個合成層)

若是你們想要更直觀地瞭解合成層到底是一個什麼樣的形式,Chrome開發者工具爲咱們提供了十分好用的工具。即是開發者工具中的Layers功能模塊(具體的添加及使用流程已在前文中作了詳細介紹,若有須要還望讀者移步):

版塊的左側的列表裏將會列出頁面裏存在哪些渲染層,右側的Details將會顯示這些渲染層的詳細信息。包括渲染層的大小、造成緣由等等,從圖中咱們能夠清楚知道,百度首頁只存在一個合成層document(由於百度首頁自己沒有過多的動畫須要大量重排重繪,因此一個合成層足夠了),這個合成成的造成緣由是由於它是一個根Layer(Root Layer),和咱們說的造成合成層的第一個條件別無二致。

你們能夠試着在開發者工具里根據咱們剛剛提出的幾條規則試着去修改元素的CSS樣式,嘗試一下看看是否會生成一個新的Compositing Layer。↖(^ω^)↗

不過這時候問題來了,爲何咱們已經對RenderObject合成了一次RenderLayer,以後還須要再合成一次Compositing Layer呢,這難道不是畫蛇添足嗎?其實緣由是,首先咱們再一次對頁面的層級進行了一次合成,這樣能夠減小內存的使用量;其二是在合併以後,GPU會盡可能減小合併後元素髮生更新帶來的重排重繪性能和處理上的困難。

上面的兩個緣由你們聽起來可能還雲裏霧裏,到底是什麼意思呢?

咱們都知道,提高渲染性能的第一要義是減小重排重繪,咱們以前也說過,在軟件渲染的過程當中,若是發生元素更新,CPU須要找到更新到RenderObject進行從新繪製,其中過程包括了重排和重繪。但若是頁面只是某個合成層發生了位置的偏移、縮放、透明度變化等操做,那麼GPU會取代CPU去處理從新繪製的工做,由於GPU要作的知識把更新的合成層進行相應的變換並送入Compositor從新合成便可。

PS:你們能夠嘗試的本身寫一個動畫,好比某個div從left: 0 變化到 left: 200px ,若是觸發了合成層它是不會發生重排和重繪。(觀察元素是否發生了重排重繪的方法已在前文進行了詳細介紹)

綜上所述,瀏覽器的渲染方式大概是下面這樣一個流程:

筆者本身畫的流程圖可能比較簡陋,但願你們見諒啊。也就是說,網頁加載後,每當從新繪製新的一幀的時候,須要經歷三個階段,就是流程圖中的佈局、繪製和合成三個階段。而且,layout和paint每每佔用了大量的時間,因此咱們想要提升性能,就必須儘量減小布局和繪製的時間,最佳的解決方案固然是在從新渲染時觸發硬件加速而直接跳太重排和重繪的過程。

8.【拓展】JS性能監測

自從前文發佈後,就有小夥伴向我提到了JS阻塞性能這部份內容介紹的較少,今天就爲此做些許補充。你們都知道JS代碼會阻塞咱們的頁面渲染,並且相對於另外兩部分性能優化而言(前文提到過的網絡傳輸性能優化與頁面渲染性能優化),JS性能調優是一項很大的工程,由於做爲一門編程語言,其中涉及到的算法、時間複雜度等知識對於大多數CS專業的學生而言應該是很熟悉的名詞了吧,這也是大廠筆試面試必考的知識點。舉個最簡單的例子,學過C的小夥伴確定熟悉這麼一個梗,請輸出給定範圍(N)內全部的素數,你可能會想到使用兩個for循環去實現,的確,這樣輸出的值沒有一點問題,可是沒有做任何優化,作過這道題的人都知道能夠在內層的for循環裏將區間限制在j<=(int)sqrt(i) 這句簡單的代碼有什麼效果呢,給你舉個簡單的例子,若是N的取值是100,它能幫你省去內層循環最多90次的執行,具體原理你們就自行去研究吧。

若是你對這些計算機基礎知識還不是特別瞭解,或者以前沒有傳統編程語言的基礎,我推薦你們去翻閱這樣一篇文章,可以快速地帶你瞭解關於代碼執行性能的重要指標——時間複雜度的相關知識。傳送門:mp.weixin.qq.com/s?__biz=MzA…

而這個模塊的內容,不會給你們去介紹JS經常使用的算法或者是下降複雜度的技巧,由於若是我這麼一篇簡短的文章可以說得清楚的話,這些知識在大學裏面就不會造成一門完整的課程了。今天主要就是爲你們推薦兩款很是實用的JS代碼性能監測工具,供你們比較本身與他人書寫的代碼的性能優劣。

8.1.Benchmark.js

首先提到的即是聲名遠播的Benchmark.js這款插件啦,這是它的官網:benchmarkjs.com/ (圖片來自官網截圖)

使用方法很簡單,按照官網的教程一步步走就好了:

  • 首先如今項目裏安裝Benchmark:$ npm i --save benchmark

  • 在檢測文件中引入Benchmark模塊:var Benchmark = require('benchmark');

  • 實例化Benchmark下的Suite,使用實例下的add方法添加函數執行句柄

  • 實例的on方法就是用於監聽Benchmark監測代碼執行拋出的事件,其中cycle會在控制檯輸出相似這樣的執行結果:

其中,Ops/sec 測試結果以每秒鐘執行測試代碼的次數(Ops/sec)顯示,這個數值確定是越大越好。除了這個結果外,同時會顯示測試過程當中的統計偏差(百分比值)。

  • 若是你手動設置了監聽了complete事件,經過示例上的方法就能夠幫你自動比較出執行效率較高的函數句柄。

Benchmark的使用方法就是這麼簡便,它的做用就好像是咱們平時運動會短跑比賽上裁判的讀秒器,而咱們的代碼就像是咱們的運動員,試着去和大家的小夥伴比比看,看實現同一需求,誰的代碼更有效率吧。

8.2.JsPerf

JsPerf和Benchmark的功能其實是如出一轍的,包括它的輸出內容,只不過它是一款在線的代碼執行監測工具,無需像Benchmark那樣安裝模塊,書寫本地文件,只須要簡單的複製粘帖就行,傳送門:jsperf.com/ (圖片來自官網截圖)

咱們只須要使用github登錄,而後點擊Tests下的Add連接就可疑新建一個監測項目

接下來會讓咱們填一些描述信息,基本的英文你們應該都能看懂吧,這就不用我再去介紹了,只要把帶星號的部分填完就沒問題了:

重點就是把Code snippets to compare這個模塊裏面的內容天完整就好了,顧名思義,這裏面填寫的就是咱們須要去監測的兩個代碼執行句柄。

點擊save test case監測結果就是這樣,具體評判標準參照Benchmark:

9.結語

花了三天的時間才終於把瀏覽器的渲染機制這篇文章的相關內容整理完成,筆者也是創建在本身粗略的理解上將本身總結的經驗分享給你們,這篇文章比前文寫起來難度要高不少,由於所涉及的理論和知識太深,又只有太少的素材對這些理論展開了深刻的介紹,但在咱們實際的開發中,若是隻知其然而不知其因此然,每每會在不少地方陷入迷茫,或者濫用硬件加速形成移動產品不可逆轉的壽命消耗,因此筆者在研讀完《WebKit技術內幕》一書以後,便馬上將書中知識結合開發所學撰寫成文,與廣大前端愛好者分享。若是文中有歧義或者錯誤,歡迎你們在評論區提出意見和批評,我會第一時間回答和改正。成文不易,不喜勿噴。

相關文章
相關標籤/搜索