前端開發者應知必會:瀏覽器是如何渲染網頁的
原文鏈接:What Every Frontend Developer Should Know About Webpage Rendering
作者:Alexander Skutin
譯者:余博倫
轉載請註明出處。
今天我們討論的話題將專註於網頁渲染以及它在Web開發中至關重要的作用。其實網上已經有許多談論這個主題的文章了,但大多數文章提供的都是比較碎片化的信息,我需要查閱相當多的資料,才能完整地了解網頁渲染。所以我決定寫下這篇有一定綜合性的文章。相信一方面能夠幫助初學者了解網頁渲染的原理,另一方面也能幫助有經驗的同學細化鞏固相關的知識結構。
不同的瀏覽器引擎運行起來會有些許差異,針對特定瀏覽器的具體內容會更加複雜。本文並不會涉及某個瀏覽器的底層原理,而是討論一些通共的原則。
瀏覽器如何渲染網頁
我們先來了解一下瀏覽器是如何對網頁進行渲染的:
- 瀏覽器將從伺服器獲取的HTML文檔構建成文檔對象模型DOM(Document Object Model).
- 樣式將被載入和解析,構成層疊樣式表模型CSSOM(CSS Object Model).
- 在DOM和CSSOM之上,渲染樹(rendering tree)將會被創建,代表一系列將被渲染的對象(這在Webkit內核中被稱為renderer或者渲染對象render object,在Gecko內核中被稱為框架frame)。渲染樹映射除了不可見元素(例如<head>或者含有display:none;的標籤)外的所有DOM結構。每一段文本字元串都將劃分在不同的渲染對象中,每一個渲染對象都包含了它相應的DOM對象以及計算後的樣式。換句話講,渲染樹是DOM的直觀表示。
- 渲染樹的每個元素包含的內容都是計算過的,它被稱之為布局layout.瀏覽器使用一種流式處理的方法,只需要一次pass繪製操作就可以布局所有的元素(tables需要多次pass繪製,pass表示像素處理和頂點處理)。
- 最後布局完成,渲染樹將轉化為屏幕上的實際內容,這一步被稱為繪製painting.
重繪Repaint
當頁面元素樣式的改變不影響元素在文檔流中的位置時(例如background-color, border-color,visibility),瀏覽器只會將新樣式賦予元素並進行重繪操作。
迴流Reflow
當改變影響文檔內容或者結構,或者元素位置時,迴流操作就會被觸發,一般有以下幾種情況:
- DOM操作(對元素的增刪改,順序變化等);
- 內容變化,包括表單區域內的文本改變;
- CSS屬性的更改或重新計算;
- 增刪樣式表內容;
- 修改class屬性;
- 瀏覽器窗口變化(滾動或縮放);
- 偽類樣式激活(:hover等)。
瀏覽器如何優化渲染
瀏覽器本身會儘可能地減少其重繪或迴流的次數,只更改必要的元素。例如一個position設置為absolute/fixed的元素的更改只會影響其本身和其子元素,而static的元素變化則會影響其之後的所有頁面元素。
另外一項優化的技術則是在JavaScript代碼運行時,瀏覽器會緩存所有的變化,然後只通過一次pass繪製操作來應用這些更改。例如下面這段代碼只會觸發一次重繪和迴流:
var $body = $(body);n$body.css(padding, 1px); // 觸發重繪與迴流n$body.css(color, red); // 觸發重繪n$body.css(margin, 2px); // 觸發重繪與迴流n// 最終只有一次重繪和迴流被觸發n
然而,根據我們之前提到過的,獲取某個元素的屬性將會觸發強制迴流。比如我們在剛才的代碼中加上一句讀取元素屬性的操作:
var $body = $(body);n$body.css(padding, 1px);n$body.css(padding); // 此處觸發強制迴流n$body.css(color, red);n$body.css(margin, 2px);n
結果就會有兩次迴流發生。因此,我們應該盡量合併讀取元素屬性的操作來優化性能。
當然也有我們不得不觸發強制迴流的情況。比如說對同一個元素的margin-left屬性進行兩次操作——開始的時候賦值100px的距離,之後為了實現動畫效果,再加上transition屬性將距離改變到50px.
我們先定義一個CSS類:
.has-transition {n -webkit-transition: margin-left 1s ease-out;n -moz-transition: margin-left 1s ease-out;n -o-transition: margin-left 1s ease-out;n transition: margin-left 1s ease-out;n}n
之後再對頁面元素進行操作:
// 我們的元素開始默認含有 "has-transition" 的class屬性nvar $targetElem = $(#targetElemId);nn// 移除默認的 "has-transition"n$targetElem.removeClass(has-transition);nn// 此處的屬性改變沒有動畫效果n$targetElem.css(margin-left, 100);nn// 再加上原來的屬性名n$targetElem.addClass(has-transition);nn// 這次改變有動畫效果n$targetElem.css(margin-left, 50);n
但事實上這段代碼並不會像注釋描述的那樣運作,每條語句的操作將被緩存,只有結果會在頁面上顯示,所以我們就需要手動進行一次強制迴流:
// 移除默認的 "has-transition"n$(this).removeClass(has-transition);nn// 此處的屬性改變沒有動畫效果n$(this).css(margin-left, 100);nn// 觸發強制迴流,上述兩條語句的效果會馬上在頁面中顯示n$(this)[0].offsetHeight; // 只是舉個例子,別的觸發方法也可以nn// 再加上原來的屬性名n$(this).addClass(has-transition);nn// 這次改變有動畫效果n$(this).css(margin-left, 50);n
你可以在JSBin預覽這個例子。
優化渲染效率的幾條最佳實踐
根據我查閱的一些資料,總結出以下幾條優化建議:
- 合法地書寫HTML和CSS,不要忘了文檔編碼類型。樣式文件應當在 <head> 標籤中,腳本文件在 <body> 結束前。
- 簡化並優化你的CSS選擇器(有些人可能CSS預處理器用習慣了從來不關注這一點)。將嵌套層減少到最小。CSS選擇器根據其優先順序具有不同的運行效率(從快到慢):
- ID選擇器: #id
- 類選擇器: .class
- 標籤選擇器: div
- 相鄰選擇器: a + i
- 子元素選擇器: ul > li
- 通用選擇器: *
- 屬性選擇器: input[type="text"]
- 偽類選擇器: a:hover
瀏覽器中CSS選擇器是從右到左進行匹配的(為什麼瀏覽器要從右到左匹配樣式選擇器),這也是為什麼越短的選擇器運行越快的原因(別提通用選擇器,它會遍歷所有元素):
div * {...} // ×n.list li {...} // ×n.list-item {...} // √n#list .list-item {...} // √n
- 在你的腳本代碼中,盡量減少DOM操作。緩存所有的內容,包括屬性和對象(如果他們需要被複用的話)。盡量將元素緩存到本地之後再進行操作,最後再添加到DOM當中。
- 如果你使用jQuery進行DOM操作的話,最好遵循jQuery最佳實踐。
- 修改元素樣式時,更改其class屬性是性能最高的方法。你的選擇器越有針對性越好(這同樣也有助於分離頁面樣式和邏輯)。
- 盡量只對 position 為 absolute/fixed 的元素設置動畫。
- 在頁面滾動時禁用 :hover 樣式效果:
.disable-hover {n pointer-events: none;n}n
var body = document.body,n timer;nnwindow.addEventListener(scroll, function() {n clearTimeout(timer);n if(!body.classList.contains(disable-hover)) {n body.classList.add(disable-hover)n }n n timer = setTimeout(function(){n body.classList.remove(disable-hover)n },500);n}, false);n
如果你想對此話題進行更深入的了解,可以查閱:
- How browsers work 中文版
- 【譯】瀏覽器渲染:repaint,reflow/relayout,restyle
有任何好的意見或者是建議以及心得體會歡迎在評論區參與討論。
你也可以加入本專欄QQ群一起交流學習。
歡迎加入從零學習前端開發,群號碼:591950591
更加歡迎將你的原創或翻譯文章投稿至本專欄。
推薦閱讀:
※「每日一題」CSRF 是什麼?
※從零學習前端開發·HTML
※你可能不知道的 css 內容塊
※Webpack 速成
※[翻譯]React還是Vue:你該如何選擇?