淺析前端頁面渲染機制

作爲一個前端開發,最常見的運行環境應該是瀏覽器吧,爲了更好的通過瀏覽器把優秀的產品帶給用戶,也爲了更好的發展自己的前端職業之路,有必要了解從我們在瀏覽器地址欄輸入網址到看到頁面這期間瀏覽器是如何進行工作的,進而瞭解如何更好的優化實踐,本篇主要圍繞這兩點展開闡述。前端頁面渲染機制可謂是老生常談,但又很有必要再談的話題,於是還是決定寫一篇,即是對知識的回顧總結,又能與大家分享,何樂而不爲。網上相關類型的文章也很多,有興趣的可以多學習一下。

瀏覽器

在介紹瀏覽器工作流程之前,先了解一下主流瀏覽器的基礎結構,本文所介紹的瀏覽器主要爲開源的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結構,於是瀏覽器以同步方式解析,加載和執行腳本,瀏覽器在解析文檔時,當解析到<script>標籤時,會解析其中的腳本(對於外鏈的JavaScript文件,需要先加載該文件內容,再進行解析),然後立即執行,這整個過程都會阻塞文檔解析,直到腳本執行完纔會繼續解析文檔。就是說由於腳本是同步加載和執行的,它會阻塞文檔解析,這也解釋了爲什麼現在通常建議將<script>標籤放在標籤前面,而不是放在<head>標籤裏。現在HTML5提供defer和async兩個屬性支持延遲和異步加載JavaScript文件,如:<script defer src="script.js"> 

  • 改進

針對上文說的腳本阻塞文檔解析,主流瀏覽器如Chrome和FireFox等都有一些優化,比如在執行腳本時,開啓另一個進程解析剩餘的文檔以找出並加載其他的待下載外部資源(不改變主進程的DOM樹,僅優化加載外部資源)。

  • 樣式

不同於腳本,瀏覽器對樣式的處理並不會阻塞文檔解析,大概是因爲樣式表並不會改變DOM結構。

  • 樣式表與腳本

你可能想問樣式是否會阻塞腳本文件的加載執行呢?正常情況是不會的,但是存在一個問題是通常我們會在腳本中請求樣式信息,但是在文檔解析時,如果樣式尚未加載或解析,將會得到錯誤信息,對於這一問題,FireFox瀏覽器和Webkit瀏覽器處理策略不同:
當存在有樣式文件未被加載和解析時,FireFox瀏覽器會阻塞所有腳本;
而Webkit瀏覽器只會阻塞操作了改文件內聲明的樣式屬性的腳本。

構建DOM樹

DOM,即文檔對象模型(Document Object Model),DOM樹,即文檔內所有節點構成的一個樹形結構。

假設瀏覽器獲取返回的如下HTML文檔:

  <!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樣式內容如下:

html, body {
    width: 100%;
    height: 100%;
    background-color: #fcfcfc;
    }
    .title {
    font-size: 20px;
    }
    .footer {
    font-size: 12px;
    color: #aaa;
    }


構建CSSOM樹如圖:

執行JavaScript

上文已經闡述了文檔解析時對腳本的處理,我們得知腳本加載,解析和執行會阻塞文檔解析,而在特殊情況下樣式的加載和解析也會阻塞腳本,所以現在推薦的實踐是<script>標籤放在</body>標籤前面。

構建渲染樹(render tree)

DOM樹和CSSOM樹都構建完了,接着瀏覽器會構建渲染樹:

渲染樹,代表一個文檔的視覺展示,瀏覽器通過它將文檔內容繪製在瀏覽器窗口,展示給用戶,它由按順序展示在屏幕上的一系列矩形對象組成,這些矩形對象都帶有字體,顏色和尺寸,位置等視覺樣式屬性。對於這些矩對象,FireFox稱之爲框架(frame),Webkit瀏覽器稱之爲渲染對象(render object, renderer),後文統稱爲渲染對象。

這裏把渲染樹節點稱爲矩形對象,是因爲,每一個渲染對象都代表着其對應DOM節點的CSS盒子,該盒子包含了尺寸,位置等幾何信息,同時它指向一個樣式對象包含其他視覺樣式信息。

渲染樹與DOM樹

每一個渲染對象都對應着DOM節點,但是非視覺(隱藏,不佔位)DOM元素不會插入渲染樹,如<head>元素或聲明display: none;的元素,渲染對象與DOM節點不是簡單的一對一的關係,一個DOM可以對應一個渲染對象,但一個DOM元素也可能對應多個渲染對象,因爲有很多元素不止包含一個CSS盒子,如當文本被折行時,會產生多個行盒,這些行會生成多個渲染對象;又如行內元素同時包含塊元素和行內元素,則會創建一個匿名塊級盒包含內部行內元素,此時一個DOM對應多個矩形對象(渲染對象)。

渲染樹及其對應DOM樹如圖:


佈局(Layout)或迴流(reflow,relayout)

創建渲染樹後,下一步就是佈局(Layout),或者叫回流(reflow,relayout),這個過程就是通過渲染樹中渲染對象的信息,計算出每一個渲染對象的位置和尺寸,將其安置在瀏覽器窗口的正確位置,而有些時候我們會在文檔佈局完成後對DOM進行修改,這時候可能需要重新進行佈局,也可稱其爲迴流,本質上還是一個佈局的過程,每一個渲染對象都有一個佈局或者回流方法,實現其佈局或迴流。

流(flow)

HTML採用的是基於流的方式定位佈局,其按照從左到右,從上到下的順序進行排列,詳見CSS定位機制

全局佈局與局部佈局

對渲染樹的佈局可以分爲全局和局部的,全局即對整個渲染樹進行重新佈局,如當我們改變了窗口尺寸或方向或者是修改了根元素的尺寸或者字體大小等;而局部佈局可以是對渲染樹的某部分或某一個渲染對象進行重新佈局。


髒位系統(dirty bit system)

大多數web應用對DOM的操作都是比較頻繁,這意味着經常需要對DOM進行佈局和迴流,而如果僅僅是一些小改變,就觸發整個渲染樹的迴流,這顯然是不好的,爲了避免這種情況,瀏覽器使用了髒位系統,只有一個渲染對象改變了或者某渲染對象及其子渲染對象髒位值爲”dirty”時,說明需要回流。

表示需要佈局的髒位值有兩種:

  • “dirty”–自身改變,需要回流
  • “children are dirty”–子節點改變,需要回流

佈局過程

佈局是一個從上到下,從外到內進行的遞歸過程,從根渲染對象,即對應着HTML文檔根元素<html>,然後下一級渲染對象,如對應着<body>元素,如此層層遞歸,依次計算每一個渲染對象的幾何信息(位置和尺寸)。

幾何信息-位置和尺寸,即相對於窗口的座標和尺寸,如根渲染對象,其座標爲(0, 0),尺寸即是視口
尺寸(瀏覽器窗口的可視區域)。

每一個渲染對象的佈局流程基本如:

  • 1.計算此渲染對象的寬度(width);
  • 2.遍歷此渲染對象的所有子級,依次:
    • 2.1設置子級渲染對象的座標
    • 2.2判斷是否需要觸發子渲染對象的佈局或迴流方法,計算子渲染對象的高度(height)
  • 3.設置此渲染對象的高度:根據子渲染對象的累積高,margin和padding的高度設置其高度;
  • 4.設置此渲染對象髒位值爲false。

強制迴流

在渲染樹佈局完成後,再次操作文檔,改變文檔的內容或結構,或者元素定位時,會觸發迴流,即需要重新佈局,如請求某DOM的”offsetHeight”樣式信息等諸多情況:

  • DOM操作,如增加,刪除,修改或移動;
  • 變更內容;
  • 激活僞類;
  • 訪問或改變某些CSS屬性(包括修改樣式表或元素類名或使用JavaScript操作等方式);
  • 瀏覽器窗口變化(滾動或尺寸變化)
 $('body').css('padding'); // reflow
    $('body')[0].offsetHeight; // relow

有過CSS3動畫開發經驗的同學可能會有經歷,如下入場動畫: 

.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;
    }

然後執行如下腳本:

var $slide = $('.slide-left');
    $slide.css({
        "margin-left": "100px"
    }).addClass('slide-left');
    $slide.css({
        "margin-left": "10px"
    });
我們會發現並沒有效果,爲什麼呢?因爲對margin-left的修改並沒有觸發迴流,元素margin-left值的改變被緩存,如果我們在中間強制觸發迴流:
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或字體顏色時會觸發全局或局部重繪,如:

$('body').css('color', 'red'); // repaint
$('body').css('margin', '2px'); // reflow, repaint

頁面渲染優化

瀏覽器對上文介紹的關鍵渲染路徑進行了很多優化,針對每一次變化產生儘量少的操作,還有優化判斷重新繪製或佈局的方式等等。

在改變文檔根元素的字體顏色等視覺性信息時,會觸發整個文檔的重繪,而改變某元素的字體顏色則只觸發特定元素的重繪;改變元素的位置信息會同時觸發此元素(可能還包括其兄弟元素或子級元素)的佈局和重繪。某些重大改變,如更改文檔根元素<html>的字體尺寸,則會觸發整個文檔的重新佈局和重繪,據此及上文所述,推薦以下優化和實踐:

  • 1.HTML文檔結構層次儘量少,最好不深於六層;
  • 2.腳本儘量後放,放在</body>前即可;
  • 3.少量首屏樣式內聯放在<head>標籤內;
  • 4.樣式結構層次儘量簡單;
  • 5.在腳本中儘量減少DOM操作,儘量緩存訪問DOM的樣式信息,避免過度觸發迴流;
  • 6.減少通過JavaScript代碼修改元素樣式,儘量使用修改class名方式操作樣式或動畫;
  • 7.動畫儘量使用在絕對定位或固定定位的元素上;
  • 8.隱藏在屏幕外,或在頁面滾動時,儘量停止動畫;
  • 9.儘量緩存DOM查找,查找器儘量簡潔;
  • 10.涉及多域名的網站,可以開啓域名預解析

實例

當我們訪問一個頁面時,瀏覽器渲染事件詳細日誌圖如下:


  • 1.發起請求;
  • 2.解析HTML;
  • 3.解析樣式;
  • 4.執行JavaScript;
  • 5.佈局;

轉載至:https://zhuanlan.zhihu.com/p/26105913?group_id=831106735610662912

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章