作為一個前端開發,最常見的執行環境應該是瀏覽器吧,為了更好的通過瀏覽器把優秀的產品帶給使用者,也為了更好的發展自己的前端職業之路,有必要了解從我們在瀏覽器位址列輸入網址到看到頁面這期間瀏覽器是如何進行工作的,進而瞭解如何更好的優化實踐,本篇主要圍繞這兩點展開闡述。前端頁面渲染機制可謂是老生常談,但又很有必要再談的話題,於是還是決定寫一篇,即是對知識的回顧總結,又能與大家分享,何樂而不為。網上相關型別的文章也很多,有興趣的可以多學習一下。
瀏覽器
在介紹瀏覽器工作流程之前,先了解一下主流瀏覽器的基礎結構,本文所介紹的瀏覽器主要為開源的Chrome,FireFox及部分開源的Safari,這也是目前市場佔比最高的幾大瀏覽器,以本人部落格網站為例,可以大致看出各瀏覽器使用比例:
瀏覽器基礎結構
瀏覽器基礎結構主要包括如下7部分:
- 1.使用者介面(User Interface):使用者所看到及與之互動的功能元件,如位址列,返回,前進按鈕等;
- 2.瀏覽器引擎(Browser engine):負責控制和管理下一級的渲染引擎;
- 3.渲染引擎(Rendering engine):負責解析使用者請求的內容(如HTML或XML,渲染引擎會解析HTML或XML,以及相關CSS,然後返回解析後的內容);
- 4.網路(Networking):負責處理網路相關的事務,如HTTP請求等;
- 5.UI後端(UI backend):負責繪製提示框等瀏覽器元件,其底層使用的是作業系統的使用者介面;
- 6.JavaScript直譯器(JavaScript interpreter):負責解析和執行JavaScript程式碼;
- 7.資料儲存(Data storage):負責持久儲存諸如cookie和快取等應用資料。
瀏覽器核心
各大主要瀏覽器使用核心也是有差別的,大致可以分為以下幾類:
- Trident核心: IE
- Webkit核心:Chrome,Safari
- Gecko核心:FireFox
網路
當使用者訪問頁面時,瀏覽器需要獲取使用者請求內容,這個過程主要涉及瀏覽器網路模組:
- 1.使用者在位址列輸入域名,如baidu.com,DNS(Domain Name System,域名解析系統)伺服器根據輸入的域名查詢對應IP,然後向該IP地址發起請求;
- 2.瀏覽器獲得並解析伺服器的返回內容(HTTP response);
- 3.瀏覽器載入HTML檔案及檔案內包含的外部引用檔案及圖片,多媒體等資源。
DNS預解析(DNS prefetch)
瀏覽器DNS解析大多時候較快,且會快取常用域名的解析值,但是如果網站涉及多域名,在對每一個域名訪問時都需要先解析出IP地址,而我們希望在跳轉或者請求其他域名資源時儘量快,則可以開啟域名預解析,瀏覽器會在空閒時提前解析宣告需要預解析的域名,如:
多程式
我們通常說JavaScript執行是單程式的,但是瀏覽器網路部分通常是有幾個平行程式同時開啟,但是也會有
限制,一般為2-6個。
渲染引擎及關鍵渲染路徑(Critical Rendering Path)
渲染引擎所做的事是將請求內容展現給我們,預設支援HTML,XML和圖片型別,對於其他諸如PDF等型別的內容則需要安裝相應外掛,但瀏覽器的展示工作流程基本是一樣的。
通過網路模組載入到HTML檔案後渲染引擎渲染流程如下,這也通常被稱作關鍵渲染路徑(Critical Rendering Path):
- 1.構建DOM樹(DOM tree):從上到下解析HTML文件生成DOM節點樹(DOM tree),也叫內容樹(content tree);
- 2.構建CSSOM(CSS Object Model)樹:載入解析樣式生成CSSOM樹;
- 3.執行JavaScript:載入並執行JavaScript程式碼(包括內聯程式碼或外聯JavaScript檔案);
- 4.構建渲染樹(render tree):根據DOM樹和CSSOM樹,生成渲染樹(render tree);渲染樹:按順序展示在螢幕上的一系列矩形,這些矩形帶有字型,顏色和尺寸等視覺屬性。
- 5.佈局(layout):根據渲染樹將節點樹的每一個節點佈局在螢幕上的正確位置;
- 6.繪製(painting):遍歷渲染樹繪製所有節點,為每一個節點適用對應的樣式,這一過程是通過UI後端模組完成;
為了更友好的使用者體驗,瀏覽器會盡可能快的展現內容,而不會等到文件所有內容到達才開始解析和構建/佈局渲染樹,而是每次處理一部分,並展現在螢幕上,這也是為什麼我們經常可以看到頁面載入的時候內容是從上到下一點一點展現的。
流程圖
Webkit渲染引擎流程如下圖:
Gecko渲染引擎流程如下圖:
如上圖,Webkit瀏覽器和Gecko瀏覽器渲染流程大致相同,不同的是:
- 1.Webkit瀏覽器中的渲染樹(render tree),在Gecko瀏覽器中對應的則是框架樹(frame tree),渲染物件(render object)對應的是框架(frame);
- 2.Webkit中的佈局(Layout)過程,在Gecko中稱為迴流(Reflow),本質是一樣的,後文會解釋迴流的另一層含義–重新佈局;
- 3.Gecko中HTML和DOM樹中間多了一層內容池(Content sink),可以理解成生成DOM元素的工廠。
單程式
不同於網路部分的多程式渲染引擎是單執行緒工作的,意味著渲染流程是一步一步漸進完成的。
解析文件(parser HTML)
在詳細介紹瀏覽器渲染文件之前,先應該理解瀏覽器如何解析文件:解析文件的順序,對於CSS和JavaScript如何處理等。
解析順序
瀏覽器按從上到下的順序掃描解析文件;
解析樣式和指令碼
- 指令碼或許是由於通常會在JavaScript指令碼中改變文件DOM結構,於是瀏覽器以同步方式解析,載入和執行指令碼,瀏覽器在解析文件時,當解析到
標籤時,會解析其中的指令碼(對於外鏈的JavaScript檔案,需要先載入該檔案內容,再進行解析),然後立即執行,這整個過程都會阻塞文件解析,直到指令碼執行完才會繼續解析文件。就是說由於指令碼是同步載入和執行的,它會阻塞文件解析,這也解釋了為什麼現在通常建議將
標籤放在
標籤前面,而不是放在標籤裡。現在HTML5提供defer和async兩個屬性支援延遲和非同步載入JavaScript檔案,如:
1 |
<script defer src="script.js"> |
- 改進針對上文說的指令碼阻塞文件解析,主流瀏覽器如Chrome和FireFox等都有一些優化,比如在執行指令碼時,開啟另一個程式解析剩餘的文件以找出並載入其他的待下載外部資源(不改變主程式的DOM樹,僅優化載入外部資源)。
- 樣式不同於指令碼,瀏覽器對樣式的處理並不會阻塞文件解析,大概是因為樣式表並不會改變DOM結構。
- 樣式表與指令碼你可能想問樣式是否會阻塞指令碼檔案的載入執行呢?正常情況是不會的,但是存在一個問題是通常我們會在指令碼中請求樣式資訊,但是在文件解析時,如果樣式尚未載入或解析,將會得到錯誤資訊,對於這一問題,FireFox瀏覽器和Webkit瀏覽器處理策略不同:
- 當存在有樣式檔案未被載入和解析時,FireFox瀏覽器會阻塞所有指令碼;
- 而Webkit瀏覽器只會阻塞操作了改檔案內宣告的樣式屬性的指令碼。
構建DOM樹
DOM,即文件物件模型(Document Object Model),DOM樹,即文件內所有節點構成的一個樹形結構。
假設瀏覽器獲取返回的如下HTML文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!doctype html> <html> <head> <link rel="stylesheet" href="./theme.css"></link> <script src="./config.js"></script> <title>關鍵渲染路徑</title> </head> <body> <h1 class="title">關鍵渲染路徑</h1> <p>關鍵渲染路徑介紹</p> <footer>@copyright2017</footer> </body> </html> |
首先瀏覽器從上到下依次解析文件構建DOM樹,如下:
構建CSSOM樹
CSSOM,即CSS物件模型(CSS Object Model),CSSOM樹,與DOM樹結構相似,只是另外為每一個節點關聯了樣式資訊。
theme.css樣式內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
html, body { width: 100%; height: 100%; background-color: #fcfcfc; } .title { font-size: 20px; } .footer { font-size: 12px; color: #aaa; } |
構建CSSOM樹如圖:
;
執行JavaScript
上文已經闡述了文件解析時對指令碼的處理,我們得知指令碼載入,解析和執行會阻塞文件解析,而在特殊情況下樣式的載入和解析也會阻塞指令碼,所以現在推薦的實踐是標籤放在
標籤前面。
構建渲染樹(render tree)
DOM樹和CSSOM樹都構建完了,接著瀏覽器會構建渲染樹:
渲染樹,代表一個文件的視覺展示,瀏覽器通過它將文件內容繪製在瀏覽器視窗,展示給使用者,它由按順序展示在螢幕上的一系列矩形物件組成,這些矩形物件都帶有字型,顏色和尺寸,位置等視覺樣式屬性。對於這些矩物件,FireFox稱之為框架(frame),Webkit瀏覽器稱之為渲染物件(render object, renderer),後文統稱為渲染物件。
這裡把渲染樹節點稱為矩形物件,是因為,每一個渲染物件都代表著其對應DOM節點的CSS盒子,該盒子包含了尺寸,位置等幾何資訊,同時它指向一個樣式物件包含其他視覺樣式資訊。
渲染樹與DOM樹
每一個渲染物件都對應著DOM節點,但是非視覺(隱藏,不佔位)DOM元素不會插入渲染樹,如元素或宣告
display: none;
的元素,渲染物件與DOM節點不是簡單的一對一的關係,一個DOM可以對應一個渲染物件,但一個DOM元素也可能對應多個渲染物件,因為有很多元素不止包含一個CSS盒子,如當文字被折行時,會產生多個行盒,這些行會生成多個渲染物件;又如行內元素同時包含塊元素和行內元素,則會建立一個匿名塊級盒包含內部行內元素,此時一個DOM對應多個矩形物件(渲染物件)。
渲染樹及其對應DOM樹如圖:
- 圖中渲染樹viewport即視口,是文件的初始包含塊,scroll代表滾動區域,詳見CSS之視覺格式化模型(Visual Formatting Model)
- 渲染樹並不會包含顯式或隱式地
display:none;
的標籤元素。
佈局(Layout)或迴流(reflow,relayout)
建立渲染樹後,下一步就是佈局(Layout),或者叫回流(reflow,relayout),這個過程就是通過渲染樹中渲染物件的資訊,計算出每一個渲染物件的位置和尺寸,將其安置在瀏覽器視窗的正確位置,而有些時候我們會在文件佈局完成後對DOM進行修改,這時候可能需要重新進行佈局,也可稱其為迴流,本質上還是一個佈局的過程,每一個渲染物件都有一個佈局或者回流方法,實現其佈局或迴流。
流(flow)
HTML採用的是基於流的方式定位佈局,其按照從左到右,從上到下的順序進行排列,詳見CSS定位機制。
全域性佈局與區域性佈局
對渲染樹的佈局可以分為全域性和區域性的,全域性即對整個渲染樹進行重新佈局,如當我們改變了視窗尺寸或方向或者是修改了根元素的尺寸或者字型大小等;而區域性佈局可以是對渲染樹的某部分或某一個渲染物件進行重新佈局。
髒位系統(dirty bit system)
大多數web應用對DOM的操作都是比較頻繁,這意味著經常需要對DOM進行佈局和迴流,而如果僅僅是一些小改變,就觸發整個渲染樹的迴流,這顯然是不好的,為了避免這種情況,瀏覽器使用了髒位系統,只有一個渲染物件改變了或者某渲染物件及其子渲染物件髒位值為”dirty”時,說明需要回流。
表示需要佈局的髒位值有兩種:
- “dirty”–自身改變,需要回流
- “children are dirty”–子節點改變,需要回流
佈局過程
佈局是一個從上到下,從外到內進行的遞迴過程,從根渲染物件,即對應著HTML文件根元素,然後下一級渲染物件,如對應著
元素,如此層層遞迴,依次計算每一個渲染物件的幾何資訊(位置和尺寸)。
幾何資訊-位置和尺寸,即相對於視窗的座標和尺寸,如根渲染物件,其座標為(0, 0),尺寸即是視口
尺寸(瀏覽器視窗的可視區域)。
每一個渲染物件的佈局流程基本如:
- 1.計算此渲染物件的寬度(width);
- 2.遍歷此渲染物件的所有子級,依次:
- 2.1設定子級渲染物件的座標
- 2.2判斷是否需要觸發子渲染物件的佈局或迴流方法,計運算元渲染物件的高度(height)
- 3.設定此渲染物件的高度:根據子渲染物件的累積高,margin和padding的高度設定其高度;
- 4.設定此渲染物件髒位值為false。
強制迴流
在渲染樹佈局完成後,再次操作文件,改變文件的內容或結構,或者元素定位時,會觸發迴流,即需要重新佈局,如請求某DOM的”offsetHeight”樣式資訊等諸多情況:
- DOM操作,如增加,刪除,修改或移動;
- 變更內容;
- 啟用偽類;
- 訪問或改變某些CSS屬性(包括修改樣式表或元素類名或使用JavaScript操作等方式);
- 瀏覽器視窗變化(滾動或尺寸變化)
1 2 |
$('body').css('padding'); // reflow $('body')[0].offsetHeight; // relow |
有過CSS3動畫開發經驗的同學可能會有經歷,如下入場動畫:
1 2 3 4 5 6 |
.slide-left { -webkit-transition: margin-left 1s ease-out; -moz-transition: margin-left 1s ease-out; -o-transition: margin-left 1s ease-out; transition: margin-left 1s ease-out; } |
然後執行如下指令碼:
1 2 3 4 5 6 7 |
var $slide = $('.slide-left'); $slide.css({ "margin-left": "100px" }).addClass('slide-left'); $slide.css({ "margin-left": "10px" }); |
我們會發現並沒有效果,為什麼呢?因為對margin-left的修改並沒有觸發迴流,元素margin-left值的改變被快取,如果我們在中間強制觸發迴流:
1 2 3 4 5 6 7 |
var $slide = $('.slide-left'); $slide.css({ "margin-left": "100px" }); console.log($slide.css('padding'); $slide.addClass('slide-left'); $slide.css({ "margin-left": "10px" }); |
再看就達到了預期效果。
繪製(painting)
最後是繪製(paint)階段或重繪(repaint)階段,瀏覽器UI元件將遍歷渲染樹並呼叫渲染物件的繪製(paint)方法,將內容展現在螢幕上,也有可能在之後對DOM進行修改,需要重新繪製渲染物件,也就是重繪,繪製和重繪的關係可以參考佈局和迴流的關係。
全域性與區域性繪製
與佈局相似,繪製也分為全域性和區域性繪製,即對整個渲染樹或某些渲染物件進行繪製。
觸發重繪
我們已經知道很多操作可能會觸發迴流,那麼什麼時候可能觸發重繪呢,通常,當改變元素的視覺樣式,如background-color,visibility,margin,padding或字型顏色時會觸發全域性或區域性重繪,如:
1 2 |
$('body').css('color', 'red'); // repaint $('body').css('margin', '2px'); // reflow, repaint |
頁面渲染優化
瀏覽器對上文介紹的關鍵渲染路徑進行了很多優化,針對每一次變化產生儘量少的操作,還有優化判斷重新繪製或佈局的方式等等。
在改變文件根元素的字型顏色等視覺性資訊時,會觸發整個文件的重繪,而改變某元素的字型顏色則只觸發特定元素的重繪;改變元素的位置資訊會同時觸發此元素(可能還包括其兄弟元素或子級元素)的佈局和重繪。某些重大改變,如更改文件根元素的字型尺寸,則會觸發整個文件的重新佈局和重繪,據此及上文所述,推薦以下優化和實踐:
- 1.HTML文件結構層次儘量少,最好不深於六層;
- 2.指令碼儘量後放,放在
前即可;
- 3.少量首屏樣式內聯放在
標籤內;
- 4.樣式結構層次儘量簡單;
- 5.在指令碼中儘量減少DOM操作,儘量快取訪問DOM的樣式資訊,避免過度觸發迴流;
- 6.減少通過JavaScript程式碼修改元素樣式,儘量使用修改class名方式操作樣式或動畫;
- 7.動畫儘量使用在絕對定位或固定定位的元素上;
- 8.隱藏在螢幕外,或在頁面滾動時,儘量停止動畫;
- 9.儘量快取DOM查詢,查詢器儘量簡潔;
- 10.涉及多域名的網站,可以開啟域名預解析
例項
當我們訪問一個頁面時,瀏覽器渲染事件詳細日誌圖如下:
- 發起請求;
- 解析HTML;
- 解析樣式;
- 執行JavaScript;
- 佈局;
- 繪製
參考:
http://taligarsiel.com/Projects/howbrowserswork1.htm
https://bitsofco.de/understanding-the-critical-rendering-path/