爲何每一個前端開發者都要理解頁面的渲染?

今天我要將關注點放到頁面渲染以及其重要性上。雖然已經有不少文章提到過這個主題了,但大部分信息都是零碎的片斷。爲了思考這件事情,我須要研究不少信息的來源。這也就是爲何我以爲我應該寫這篇文章的緣由。我相信這篇文章對新手會頗有用,而且對想刷新和鞏固他們已經瞭解的東西的高手也一樣適用。javascript

渲染應該從最開始當頁面佈局被定義時就進行優化,樣式和腳本在頁面渲染中扮演着很是重要的角色。專業人員知道一些技巧以免一些性能問題。php

這篇文章不會深刻研究瀏覽器的技術細節,而是提供一些通用的原則。不一樣瀏覽器引擎工做原理不一樣,這就使特定瀏覽器的學習更加複雜。css

瀏覽器是怎樣渲染一個頁面的?

咱們從瀏覽器渲染頁面的大概過程開始提及:java

  1. 由從服務器接收到的 HTML 造成 DOM(文檔對象模型)。
  2. 樣式被加載和解析,造成 CSSOM(CSS 對象模型)。
  3. 緊接着 DOM 和 CSSOM 建立了一個渲染樹,這個渲染樹是一些被渲染對象的集合( Webkit 分別叫它們」renderer」和」render object」,而在Gecko 引擎中叫」frame」)。除了不可見的元素(好比 head 標籤和一些有 display:none 屬性的元素),渲染樹映射了 DOM 的結構。在渲染樹中,每個文本字符串都被當作一個獨立的 renderer。每一個渲染對象都包含了與之對應的計算過樣式的DOM 對象(或者一個文本塊)。換句話說,渲染樹描述了 DOM 的直觀的表現形式。
  4. 對每一個渲染元素來講,它的座標是通過計算的,這被叫作「佈局(layout)」。瀏覽器使用一種只須要一次處理的「流方法」來佈局全部元素(tables須要屢次處理)。
  5. 最後,將佈局顯示在瀏覽器窗口中,這個過程叫作「繪製(painting)」。

重繪

當在頁面上修改了一些不須要改變定位的樣式的時候(好比background-color,border-color,visibility),瀏覽器只會將新的樣式從新繪製給元素(這就叫一次「重繪」或者「從新定義樣式」)。jquery

重排

當頁面上的改變影響了文檔內容、結構或者元素定位時,就會發生重排(或稱「從新佈局」)。重排一般由如下改變觸發:web

  • DOM 操做(如元素增、刪、改或者改變元素順序)。
  • 內容的改變,包括 Form 表單中文字的變化。
  • 計算或改變 CSS 屬性。
  • 增長或刪除一個樣式表。
  • 改變」class」屬性。
  • 瀏覽器窗口的操做(改變大小、滾動窗口)。
  • 激活僞類(如:hover狀態)。

瀏覽器如何優化渲染?

瀏覽器盡最大努力限制重排的過程僅覆蓋已更改的元素的區域。舉個例子,一個 position 爲 absolue 或 fixed 的元素的大小變化隻影響它自身和子孫元素,而對一個 position 爲 static 的元素作一樣的操做就會引發全部它後面元素的重排。瀏覽器

另外一個優化就是當運行一段Jjavascript 代碼的時候,瀏覽器會將一些修改緩存起來,而後當代碼執行的時候,一次性的將這些修改執行。舉例來講,這段代碼會觸發一次重繪和一次重排:緩存

var $body = $('body');
$body.css('padding', '1px'); // 重排, 重繪
$body.css('color', 'red'); // 重繪
$body.css('margin', '2px'); // 重排, 重繪
// 實際上只有一次重排和重繪被執行。

  

如上面所說,訪問一個元素的屬性會進行一次強制重排。若是咱們給上面的代碼加上一行讀取元素屬性的代碼,這個狀況就會出現:服務器

var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // 這裏讀取了一次元素的屬性,一次強制重排就會發生。
$body.css('color', 'red');
$body.css('margin', '2px');

上面這段代碼的結果就是,進行了兩次重排。所以,爲了提升性能,你應該講讀取元素屬性的代碼組織在一塊兒(細節的例子能夠看JSBin上的代碼)。babel

有一種狀況是必須觸發一次強制重排的。例如:給元素改變同一個屬性兩次(好比margin-left),一開始設置100px,沒有動畫,而後經過動畫的形式將值改成50px。具體能夠看例子,固然,我在這裏會講更多的細節。

咱們從一個有transition的CSS class開始:

.has-transition {
   -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;
}

而後進行實現:

//咱們的元素默認有"has-transition"屬性
var $targetElem = $('#targetElemId');
 
//刪除包含transition的class
$targetElem.removeClass('has-transition');
 
// 當包含transition的class已經沒了的時候,改變元素屬性
$targetElem.css('margin-left', 100);
 
// 再將包含transition的class添加回來
$targetElem.addClass('has-transition');
 
// 改變元素屬性
$targetElem.css('margin-left', 50);

上面的實現沒有按照指望的運行。全部的修改都被瀏覽器緩存了,只在上面這段代碼的最後纔會執行。咱們須要的是一次強制重排,咱們能夠經過進行如下修改來實現:

//刪除包含transition的class
$(this).removeClass('has-transition');
 
// 改變元素屬性
$(this).css('margin-left', 100);
 
//觸發一次強制重排,從而使變化了的class或屬性可以當即執行。
$(this)[0].offsetHeight; // offsetHeight僅僅是個例子,其餘的屬性也能夠奏效。
 
// 再將包含transition的class添加回來
$(this).addClass('has-transition');
 
// 改變元素屬性
$(this).css('margin-left', 50);

如今這段代碼如咱們所指望的運行了。

實際的優化建議

彙總了一些有用的信息,我建議如下幾點:

  • 建立合法的 HTML 和 CSS ,別忘了制定文件編碼,Style 應該寫在 head 標籤中,script 標籤應該加載 body 標籤結束的位置。
  • 試着簡化和優化 CSS 選擇器(這個優化點被大多數使用 CSS 預處理器的開發者忽略了)。將嵌套層數控制在最小。如下是 CSS 選擇器的性能排行(從最快的開始):
  1. ID選擇器:#id
  2. class選擇器: .class
  3. 標籤: div
  4. 相鄰的兄弟元素:a + i
  5. 父元素選擇器: ul > li
  6. 通配符選擇器: *
  7. 僞類和僞元素: a:hover ,你應該記住瀏覽器處理選擇器是從右向左的,這也就是爲何最右面的選擇器會更快——#id或.class。
div * {...} // bad
.list li {...} // bad
.list-item {...} // good
#list .list-item {...} // good
  1. 在你的腳本中,儘量的減小 DOM 的操做。把全部東西都緩存起來,包括屬性和對象(若是它可被重複使用)。進行復雜的操做的時候,最好操做一個「離線」的元素(「離線」元素的意思是與 DOM 對象分開、僅存在內存中的元素),而後將這個元素插入到 DOM 中。
  2. 若是你使用 jQuery,遵循jQuery 選擇器最佳實踐
  3. 要改變元素的樣式,修改「class」屬性是最高效的方式之一。你要改變 DOM 樹的層次越深,這一條就越高效(這也有助於將表現和邏輯分開)。
  4. 儘量的只對 position 爲 absolute 或 fix 的元素作動畫。
  5. 當滾動時禁用一些複雜的 :hover動畫是一個很好的主意(例如,給 body 標籤加一個 no-hover 的 class)關於這個主題的文章

想了解更多的細節,能夠看一下這些文章:

  1. How browsers work
  2. Rendering: repaint, reflow/relayout, restyle

但願這篇文章可以對你有所幫助!

原文連接: frontendbabel   翻譯: 伯樂在線 Moejser
譯文連接: http://blog.jobbole.com/72692/

相關文章
相關標籤/搜索