原文發表於知乎專欄cloudjfed 《chrome瀏覽器頁面渲染工做原理淺析》css
本篇文章基於本身對chrome瀏覽器工做原理的一些理解,重點在於頁面渲染的分析。此文會隨着我理解的深刻持續更新,如有錯誤,歡迎隨時指正!比心 (。♥‿♥。)html
參考資料重點來源於:前端
文章目錄:node
對於前端同窗來講,webkit這個名詞很是熟悉了,那麼當咱們在說chrome是Webkit內核的時候,咱們到底在說什麼?git
瀏覽器有一個重要的模塊,它主要的做用是將頁面變成可視(聽)化的圖形、音頻結果,這就是瀏覽器內核。不一樣瀏覽器有不一樣內核,經常使用的有Trident(IE)、Gecko(Firefox)、Blink(Chrome)、Webkit(Safari)github
瀏覽器內核又能夠分紅兩部分:渲染引擎和 JS 引擎。web
最開始渲染引擎和 JS 引擎並無區分的很明確,後來 JS 引擎愈來愈獨立,內核就傾向於只指渲染引擎。ajax
一說到Webkit,最早想起來的可能就是chrome。但其實Webkit最先是蘋果公司的一個開源項目。算法
蘋果同窗哭瞎了眼。chrome
Webkit項目的結構以下
(圖片來自《WebKit技術內幕》第三章)
從圖中能夠看到,Webkit主要由WebCore(渲染引擎)、JavaScriptCore(JavaScript引擎)和Webkit Port(不一樣瀏覽器本身實現的移植部分)構成。
整個項目稱爲Webkit,而咱們前端開發者在談到Webkit的時候,每每指的是WebCore,即渲染引擎部分。
當咱們打開一個頁面的時候,網頁的渲染流程以下:
(圖片來自《WebKit技術內幕》第一章)
圖中DOM和js引擎的雙向箭頭,指的是dom和js引擎的橋接接口,用於調用對方的一些方法。
那爲何咱們提到Webkit的時候,每每會和chrome聯繫在一塊兒呢?
2008 年,谷歌公司發佈了 chrome 瀏覽器,瀏覽器使用的內核被命名爲 chromium。
谷歌公司研發了本身的JavaScript引擎,v8, 但fork 了開源引擎 Webkit。後來谷歌公司在 Chromium 項目中研發 Blink 渲染引擎,也就是說,最初Blink 是從Webkit複製過來,沒有太大區別,但谷歌逐漸進行了優化,並慢慢將其中於chromium 不相關的代碼進行移除。因此能夠預見的是,之後二者差距會越來越大。
對此,我採訪了一下蘋果公司,他們表示:
可能須要給他們寄一箱原諒套餐== 固然是假的!畢竟我又不是隔壁老王!
渲染引擎顧名思義,負責渲染,它將網絡或者本地獲取的網頁和資源從字節流進行解釋,呈現出來,流程以下圖:
從圖中能夠看到,渲染引擎具體作了:
1. 用HTML 解釋器 將字節流解釋成DOM樹
HTML解釋器的工做以下:
(圖片來自《Webkit技術內幕》第五章)
解釋器進行分詞後,生成節點,並從節點生成DOM樹。
那如何從「並列」的節點,生成具備層次結構的樹呢?
解釋器在構建節點屬性的時候,使用了棧結構,即這樣一個代碼片斷<div><p><span></span></p></div>
,當解釋到span
時,此時棧中元素就是 div、p、span
,當解釋到</span>
時,span
出棧,遇到</p> p
出棧,以此類推。
固然,HTML解釋器在工做時頗有可能遇到全局的js代碼!我知道此刻你要說,那就停下來執行js代碼啊!
事實上,解釋器確實是停下來了,但並不會立刻執行js代碼,瀏覽器的預掃描和預加載機制會先掃描後面的詞語,若是發現有資源,那就會請求併發下載資源,而後,再執行js代碼。
詳細可參考:HTML5解析算法
2. CSS解釋器:把css字符串解釋後生成style rules
3. RenderObject 樹
Webkit檢查DOM樹中每一個DOM節點,判斷是否生成RenderObject對象,由於有些節點是不可見的,好比 style head 或者 display爲none的節點(如今你知道爲啥display:none和visibility:hidden爲何表現不同了吧)。RenderObject對象疊加了2中相應的css屬性。
4. 佈局(Layout)
此時的RenderObject 樹,並不包含位置和大小信息。Webkit根據模型來進行遞歸的佈局計算。因此當樣式發生變化時,就須要從新計算佈局,這很耗費性能,更糟糕的是,一旦重排就要重繪了!
5. 繪製(Paint)
佈局完,終於能夠調用方法進行繪製了!
而咱們常說的重繪(repaint),就是當這些元素的顏色、背景等發生變化時,須要進行的。
6. 複合圖層化(Composite)
事實上,網頁是有層次結構的,基於RenderObject樹,創建了 RenderLayer樹,每一個節點都是RenderLayer節點,一個RenderLayer節點上有n個RenderObject。
什麼是RenderLayer呢? 舉個栗子:好比有透明效果的RenderObject節點和使用Canvas(或WebGL技術)的RenderObject節點都須要新建一個RenderLayer。
事實上,能夠從chrome的開發面板中看到這些層,下圖是淘寶頁面的RenderLayer:
最後,瀏覽器使用GPU對這些層合成!
咱們都知道js是單線程的。爲何呢?js設計之初是爲了進行簡單的表單驗證,操做DOM,與用戶進行互動。如果多線程操做,則極有可能出現衝突,好比同時操做同一個DOM元素,那到底聽誰的?咱們固然可使用 「鎖」機制來解決這些衝突,但這提升了複雜度。畢竟,js是由js之父 Brendan Eich 花了10天開發出來的。
媽媽問我爲何跪着打下了這些字=。=
JavaScript引擎的主要做用,就是讀取文件中的JavaScript,處理它並執行。
js是一門解釋型語言。解釋型語言和編譯型語言分別由解釋器和編譯器處理,下面是二者的處理過程:
解釋型語言和編譯型語言的區別在於,它不提早編譯,或者說,你能不能拿到中間代碼。
通常的JavaScript引擎(好比JavaScriptCore)的執行過程是這樣的:
源代碼→抽象語法樹(AST)→字節碼 → JIT →本地代碼
解釋執行效率很低,由於相同的語句被反覆解釋。所以優化的思路是動態觀察哪些代碼常常被調用,對於那些被高頻率調用的代碼,就用編譯器把它編譯而且緩存下來,下次執行的時候就不用從新解釋,從而提高速度。這就是 JIT(Just-In-Time)。
4.2.2.1 v8以前的作法----機器碼
基於字節碼的實現是主流,然而v8獨闢蹊徑,它的解釋過程是這樣的
源代碼→抽象語法樹(AST)→JIT→本地代碼
v8放棄了編譯成字節碼的過程,少了AST轉化成字節碼轉化,節約了轉化時間,並且原生機器碼執行更快。在V8生成本地代碼後,也會經過Profiler採集一些信息,來優化本地代碼。換句話說,v8的作法,是犧牲空間換時間。
4.2.2.3 v8 新版本—字節碼
然而,今年4月末,v8推出了新版本,他們啓動了 Ignition 字節碼解釋器。v8又迴歸了字節碼。
講道理,機器碼既然執行快,爲何又要「回退」到字節碼呢?不能由於我超可愛,你就欺負我啊!
詳細能夠看《V8 Ignition:JS 引擎與字節碼的不解之緣》
文章做者認爲緣由以下:
1. 減輕機器碼佔用的內存空間,即犧牲時間換空間(主要動機)
字節碼是機器碼的抽象,同一段代碼,在字節碼和機器碼中的存儲以下:
(圖片來自Understanding V8’s Bytecode)
顯然,機器碼佔用內存過大
2. 提升代碼的啓動速度;
3. 對 v8 的代碼進行重構,下降 v8 的代碼複雜度
個人補充解釋以下:
JIT優化過程當中,safari的JSC的作法以下圖:
(圖片來自:[WebKit] JavaScriptCore解析)
然而,js是無類型語言,也就是變量的類型有可能會改變。舉一個典型的栗子:
function add(a, b) {
return a + b;
}
複製代碼
若是這裏的 a 和 b 都是整數,可見最終的代碼是彙編中的 add 命令。若是相似的加法運算調用了不少次,解釋器可能會認爲它值得被優化,因而編譯了這段代碼。但若是下一次調用的是 add("你好哇", "雲霽!"),以前的優化就無效了,由於字符串加法的實現和整數加法的實現徹底不一樣。
而v8以前並無字節碼這個中間表示,因此優化後的代碼(二進制格式)還得被還原成原先的形式(字符串格式),這樣的過程被稱爲優化回滾。反覆的優化 -> 優化回滾 -> 優化 …… 很是耗時,大大下降了引入 JIT 帶來的性能提高。
因而JIT 就很難過
而如今的v8 使用 Ignition(字節碼解釋器) 加 TurboFan(JIT 編譯器)的組合,緩解了這個問題
先後性能對好比下圖:
(圖片來自:emm...找不到出處了,有好心人知道望告知)
js是單線程的,但爲何能執行ajax和setTimeout等異步操做呢? 很簡單,由於瀏覽器是多線程的呀!
一個瀏覽器一般由如下線程組成:
5.2.1 我是一段野生代碼
咱們先來看一段代碼
var init = new Date().getTime()
function a1(){
console.log('1')
}
function a2(){
console.log('2')
}
function a3(){
console.log('3')
}
function a4(){
console.log('4')
}
function a5(){
console.log('5')
}
function a6(){
console.log('6')
}
function a7(){
console.log('7')
}
function a8(){
console.log('8')
}
function a9(){
console.log('9')
}
function a10(){
for(let i = 1;i<10000;i++){}
console.log('10')
}
a1()
setTimeout(() => {
a2()
console.log(new Date().getTime()-init)
Promise.resolve().then(() => {
a3()
}).then(() => {
a4()
})
a5()
}, 1000)
setTimeout(()=>{
a6()
console.log(new Date().getTime()-init)
}, 0)
Promise.resolve().then(() => {
a7()
}).then(() => {
a8()
})
a9()
a10()
複製代碼
之因此有n個a*函數,是爲了後續方便調試,核心代碼從a1()開始
執行結果:你猜?
代碼裏用到了定時器和異步請求,那麼他們究竟是怎麼配合執行的呢?
這裏須要引入一個概念,event loop。
5.2.2.1 事件機制的概念
瀏覽器的主線程是event loop即事件循環,什麼是eventloop呢?
HTML5規範是這麼說的
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
爲了協調事件、用戶交互、腳本、UI 渲染、網絡請求,用戶代理必須使用 eventloop。
5.2.2.2 事件機制的原理
理解事件循環機制的工做原理是這樣的:
咱們基於規範學習一下這幾個名詞:
task queue(任務隊列)
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for work as...
一個事件循環會有一個或者多個任務隊列,每一個任務隊列都是一系列任務按照順序組成的列表。
而多個任務列表源於:每一個任務都有指定的任務源,相同的任務源的任務按順序放在同一個任務列表裏。不一樣的任務列表按照優先級執行任務。
哪些是task任務源呢?
規範在Generic task sources中有說起(原文可看連接,爲節省篇幅,此處直接給出翻譯):
DOM操做任務源
此任務源用於對DOM操做做出反應,例如一個元素以非阻塞的方式插入文檔。
用戶交互任務源
此任務源用於對用戶交互做出反應,例如鍵盤或鼠標輸入
響應用戶操做的事件(例如 click)必須使用task隊列。
網絡任務源
此任務源用於響應網絡活動。
歷史遍歷任務源
此任務源用於將對history.back()和相似API的調用排隊
此外 setTimeout、setInterval、IndexDB 數據庫操做等也是任務源。
Microtask
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.
一個事件循環會有一個microtask列表,microtask中的任務一般指:
簡單來講,事件循環機制是這樣運行的(此處規範原文):
5.2.3.1 分析
根據以上理論,咱們很容易分析到上述代碼執行的事件循環,以下:
執行棧讀到script,開始執行任務
第一次循環:
(計時線程到時間後,將計時器的回調函數按順序放入任務隊列中)
第二次循環:
從任務隊列中讀到setTimeout2 cb
由於setTimeout老是計時結束以後,在任務隊列中排隊等待執行,因此它執行的時間,老是大於等於開發者設置的時間
可是,即使設置爲0,且當前沒有正在執行的任務的狀況下,時間也不可能爲0,由於規範規定,最小時間爲4ms!
第三次循環:
從任務隊列中讀到setTimeout1 cb
5.2.3.2 驗證
好了,我說了不算,咱們用chrome developer tools的Perfomance面板來驗證是否正確
步驟是醬的:
1. 打開隱身模式,或者去掉chrome啓動的插件,由於這些插件會干擾咱們分析
2. 打開控制檯
3. 打開面板:新版chrome是Perfomance面板,老版是Timeline面板
4. 看見左上角那個實心圈圈沒有?
趁他不注意,趕忙懟他一下!
5. 如今已經開始錄製了,迅速刷新一下頁面,等個3,4s就中止錄製
6. 仔細看下面那個 Main那條來一塊兒分析。
第一次循環:
看到一個很醒目的a1(紫條)了!
a1 後面是 黃色的setTimeout(黃條),再後面是a9 a10(紫條) run microtasks(黃條),下面一次是a7 a8(紫條)
(這就是爲何要寫函數名,否則全世界都是匿名函數,乍一看還分不清誰是誰)
來鏡頭拉近看一下setTimeout那裏的兩個小黃條在作什麼
紅色框裏的文字,是鼠標移上去看到的字,橙色框是詳細信息,點擊最後一行 index.html 能夠看到具體代碼,這裏忘了截圖。戳進去會跳轉到第一個setTimeout那一行(也就是89行)。
這個是第二個setTimeout,定位是在第二個setTimeout那裏。
可驗證第一次循環判斷正確!First Blood!
第二次循環:
Double Kill!
第三次循環:
可能有人會疑惑這裏爲何沒有a4,那是由於代碼執行太快,而控制面板顯示時間是精確到0.0m的,因此會有些偏差,事實上,咱們在a3中多執行一些耗時代碼就能看到了。或者也能夠多錄製幾回,每次結果都會有些出入,可是函數執行順序是不會不一致滴!
Aced!一百昏!一百昏!老鐵們雙擊666!
說了那麼多,此時難道咱們不該該作點什麼?
《WebKit技術內幕》