前端性能優化之重排和重繪

1、重排 & 重繪

有經驗的大佬對這個概念必定不會陌生,「瀏覽器輸入URL發生了什麼」。估計你們已經爛熟於心了,從計算機網絡到JS引擎,一路飛奔到瀏覽器渲染引擎。 經驗越多就能理解的越深。javascript

感興趣的同窗能夠看一下這篇文章,深度和廣度俱佳: 《從輸入 URL 到頁面加載的過程?如何由一道題完善本身的前端知識體系!》css

切回正題,咱們繼續探討何爲重排。瀏覽器下載完頁面全部的資源後,就要開始構建DOM樹,與此同時還會構建渲染樹(Render Tree)。(其實在構建渲染樹以前,和DOM樹同期會構建Style Tree。DOM樹與Style Tree合併爲渲染樹)html

DOM樹:表示頁面的結構
渲染樹:表示頁面的節點如何顯示前端

一旦渲染樹構建完成,就要開始繪製(paint)頁面元素了。java

當DOM的變化引起了元素幾何屬性的變化,好比改變元素的寬高元素的位置,致使瀏覽器不得不從新計算元素的幾何屬性,並從新構建渲染樹,這個過程稱爲「重排」。完成重排後,要將從新構建的渲染樹渲染到屏幕上,這個過程就是「重繪」。node

簡單的說,重排負責元素的幾何屬性更新,重繪負責元素的樣式更新。並且,重排必然帶來重繪,可是重繪未必帶來重排。好比,改變某個元素的背景,這個就不涉及元素的幾何屬性,因此只發生重繪。segmentfault

2、 重排觸發機制繪

上面已經提到了,重排發生的根本原理就是元素的幾何屬性發生了改變,那麼咱們就從可以改變元素幾何屬性的角度入手瀏覽器

  1. 添加或刪除可見的DOM元素
  2. 元素位置改變
  3. 元素自己的尺寸發生改變
  4. 內容改變
  5. 頁面渲染器初始化
  6. 瀏覽器窗口大小發生改變

3、 如何進行性能優化

重繪和重排的開銷是很是昂貴的,若是咱們不停的在改變頁面的佈局,就會形成瀏覽器耗費大量的開銷在進行頁面的計算,這樣的話,咱們頁面在用戶使用起來,就會出現明顯的卡頓。如今的瀏覽器其實已經對重排進行了優化,好比以下代碼:緩存

var div = document.querySelector('.div');
div.style.width = '200px';
div.style.background = 'red';
div.style.height = '300px';

比較久遠的瀏覽器,這段代碼會觸發頁面2次重排,在分別設置寬高的時候,觸發2次.性能優化

當代的瀏覽器對此進行了優化,這種思路相似於如今流行的MVVM框架使用的虛擬DOM,對改變的DOM節點進行依賴收集,確認沒有改變的節點,就進行一次更新。可是瀏覽器針對重排的優化雖然思路和虛擬DOM接近,可是仍是有本質的區別。大多數瀏覽器經過隊列化修改並批量執行來優化重排過程。也就是說上面那段代碼其實在如今的瀏覽器優化下,只構成一次重排。

可是仍是有一些特殊的元素幾何屬性會形成這種優化失效。好比:

offsetTop, offsetLeft,...
scrollTop, scrollLeft, ...
clientTop, clientLeft, ...
getComputedStyle() (currentStyle in IE)

爲何形成優化失效呢?仔細看這些屬性,都是須要實時回饋給用戶的幾何屬性或者是佈局屬性,固然不能再依靠瀏覽器的優化,所以瀏覽器不得不當即執行渲染隊列中的「待處理變化」,並隨之觸發重排返回正確的值。

接下來深刻的介紹幾種性能優化的小TIPS

3.1 最小化重繪和重排

既然重排&重繪是會影響頁面的性能,尤爲是糟糕的JS代碼更會將重排帶來的性能問題放大。既然如此,咱們首先想到的就是減小重排重繪。

改變樣式

考慮下面這個例子:

// javascript
var el = document.querySelector('.el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

這個例子其實和上面那個例子是一回事兒,在最糟糕的狀況下,會觸發瀏覽器三次重排。然鵝更高效的方式就是合併全部的改變一次處理。這樣就只會修改DOM節點一次,好比改成使用cssText屬性實現:

var el = document.querySelector('.el');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';

沿着這個思路,聰明的老鐵必定就說了,你直接改個類名不也妥妥的。沒錯,還有一種減小重排的方法就是切換類名,而不是使用內聯樣式的cssText方法。使用切換類名就變成了這樣:

// css 
.active {
    padding: 5px;
    border-left: 1px;
    border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active';

批量修改DOM

若是咱們須要對DOM元素進行屢次修改,怎麼去減小重排和重繪的次數呢?

有的同窗又要說了,利用上面修改樣式的方法不就好了嗎。回過頭看一下形成頁面重排的幾個要點裏,能夠明確的看到,形成元素幾何屬性發生改變就會觸發重排,如今須要增長10個節點,必然涉及到DOM的修改,這個時候就須要利用批量修改DOM這種優化方式了,這裏也能看到,改變樣式最小化重繪和重排這種優化方式適用於單個存在的節點。

批量修改DOM元素的核心思想是:

  • 讓該元素脫離文檔流
  • 對其進行多重改變
  • 將元素帶回文檔中

打個比方,咱們主機硬盤出現了故障,常見的辦法就是把硬盤卸下來,用專業的工具測試哪裏有問題,待修復後再安裝上去。要是直接在主板上面用螺絲刀弄來弄去,估計主板一下子也要壞了...

這個過程引起倆次重排,第一步和第三步,若是沒有這兩步,能夠想象一下,第二步每次對DOM的增刪都會引起一次重排。那麼知道批量修改DOM的核心思想後,咱們再瞭解三種可使元素能夠脫離文檔流的方法,注意,這裏不使用css中的浮動&絕對定位,這是風馬牛不相及的概念。

看一下下面這個代碼示例:

// html
<ul id="mylist">
  <li><a href="https://www.mi.com">xiaomi</a></li>
  <li><a href="https://www.miui.com">miui</a></li>
</ul>

// javascript 如今須要添加帶有以下信息的li節點
let data = [
  {
    name: 'tom',
    url: 'https://www.baidu.com',
  },
  {
      name: 'ann',
      url: 'https://www.techFE.com'
  }
]

首先,咱們先寫一個通用的用於將新數據更新到指定節點的方法:

// javascript
function appendNode($node, data) {
  var a, li;
  
  for(let i = 0, max = data.length; i < max; i++) {
    a = document.createElement('a');
    li = document.createElement('li');
    a.href = data[i].url;
    
    a.appendChild(document.createTextNode(data[i].name));
    li.appendChild(a);
    $node.appendChild(li);
  }
}

首先咱們忽視全部的重排因素,你們確定會這麼寫:

let ul = document.querySelector('#mylist');
appendNode(ul, data);

使用這種方法,在沒有任何優化的狀況下,每次插入新的節點都會形成一次重排(這幾部分咱們都先討論重排,由於重排是性能優化的第一步)。

考慮這個場景,若是咱們添加的節點數量衆多,並且佈局複雜,樣式複雜,那麼能想到的是你的頁面必定很是卡頓。咱們利用批量修改DOM的優化手段來進行重構

1)隱藏元素,進行修改後,而後再顯示該元素

let ul = document.querySelector('#mylist');
ul.style.display = 'none';
appendNode(ul, data);
ul.style.display = 'block';

這種方法形成倆次重排,分別是控制元素的顯示與隱藏。對於複雜的,數量巨大的節點段落能夠考慮這種方法。爲啥使用display屬性呢,由於display爲none的時候,元素就不在文檔流了,還不熟悉的老鐵,手動Google一下三者的區別

display:none;
opacity: 0;
visibility: hidden

2)使用文檔片斷建立一個子樹,而後再拷貝到文檔中

let fragment = document.createDocumentFragment();
appendNode(fragment, data);
ul.appendChild(fragment);

我是比較喜歡這種方法的,文檔片斷是一個輕量級的document對象,它設計的目的就是用於更新,移動節點之類的任務,並且文檔片斷還有一個好處就是,當向一個節點添加文檔片斷時,添加的是文檔片斷的子節點羣,自身不會被添加進去。

不一樣於第一種方法,這個方法並不會使元素短暫消失形成邏輯問題。上面這個例子,只在添加文檔片斷的時候涉及到了一次重排。

3)將原始元素拷貝到一個獨立的節點中,操做這個節點,而後覆蓋原始元素

let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, data);
old.parentNode.replaceChild(clone, old);

能夠看到這種方法也是隻有一次重排。總的來講,使用文檔片斷,能夠操做更少的DOM(對比使用克隆節點),最小化重排重繪次數。

緩存佈局信息

緩存佈局信息這個概念,在《高性能JavaScript》DOM性能優化中,屢次提到相似的思想.

好比我如今要獲得頁面ul節點下面的100個li節點,最好的辦法就是第一次獲取後就保存起來,減小DOM的訪問以提高性能,緩存佈局信息也是一樣的概念。

前面有講到,當訪問諸如offsetLeftclientTop這種屬性時,會衝破瀏覽器自有的優化————經過隊列化修改和批量運行的方法,減小重排/重繪版次。因此咱們應該儘可能減小對佈局信息的查詢次數,查詢時,將其賦值給局部變量,使用局部變量參與計算。

看如下樣例:
將元素div向右下方平移,每次移動1px,起始位置100px, 100px。性能糟糕的代碼:

div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';

這樣形成的問題就是,每次都會訪問div的offsetLeft,形成瀏覽器強制刷新渲染隊列以獲取最新的offsetLeft值。更好的辦法就是,將這個值保存下來,避免重複取值

current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';
相關文章
相關標籤/搜索