1. 程式人生 > >重繪和迴流以及如何優化

重繪和迴流以及如何優化

1、瀏覽器渲染機制

瀏覽器採用流式佈局模型(Flow Based Layout)
瀏覽器會把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合併就產生了渲染樹(Render Tree)。
有了RenderTree,我們就知道了所有節點的樣式,然後計算他們在頁面上的大小和位置,最後把節點繪製到頁面上。
由於瀏覽器使用流式佈局,對Render Tree的計算通常只需要遍歷一次就可以完成,但table及其內部元素除外,他們可能需要多次計算,通常要花3倍於同等元素的時間,這也是為什麼要避免使用table佈局的原因之一。

為了構建渲染樹,瀏覽器主要完成了以下工作:
從DOM樹的根節點開始遍歷每個可見節點。
對於每個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
根據每個可見節點以及其對應的樣式,組合生成渲染樹。
第一步中,既然說到了要遍歷可見的節點,那麼我們得先知道,什麼節點是不可見的。不可見的節點包括:
一些不會渲染輸出的節點,比如script、meta、link等。
一些通過css進行隱藏的節點。比如display:none。注意,利用visibility和opacity隱藏的節點,還是會顯示在渲染樹上的。只有display:none的節點才不會顯示在渲染樹上。
注意:渲染樹只包含可見的節點.

2、迴流 (refolw)

節點的幾何屬性或者佈局發生改變被稱為迴流,一個元素的迴流可能會導致了其所有子元素以及DOM中緊隨其後的節點、祖先節點元素的隨後的迴流,所以一個節點的迴流會引起頁面某個部分甚至整個頁面的迴流。

3、重繪(repaint)

節點的樣式改變且不影響佈局的,比如color,visibility等,稱為重繪。

** 重繪不一定迴流,迴流一定重繪。 **

4、 瀏覽器優化

現代瀏覽器大多都是通過佇列機制來批量更新佈局,瀏覽器會把修改操作放在佇列中,至少一個瀏覽器重新整理(即16.6ms)才會清空佇列,但當你獲取佈局資訊的時候,佇列中可能有會影響這些屬性或方法返回值的操作,即使沒有,瀏覽器也會強制清空佇列,觸發迴流與重繪來確保返回正確的值。
主要包括以下屬性或方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect

所以,我們應該避免頻繁的使用上述的屬性,他們都會強制渲染重新整理佇列。
這裡我有思考一個問題,為什麼強制清空瀏覽器UI渲染佇列會引起效能問題?
參考這篇文章 《https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%e5%8a%a8%e7%94%bb%e7%ae%97%e6%b3%95/》 說說FPS的東西。

相當一部分的瀏覽器的顯示頻率是16.7ms, 就是上圖第一行的節奏,表現就是“我和你一步兩步三步四步往前走……”。如果我們火力搞猛一點,例如搞個10ms setTimeout,就會是下面一行的模樣——每第三個圖形都無法繪製(紅色箭頭指示),表現就是“我和你一步兩步 坑 四步往前走……”。
國慶北京高速,最多每16.7s通過一輛車,結果,突然插入一批setTimeout的軍車,強行要10s通過。顯然,這是超負荷的,要想順利進行,只能讓第三輛車直接消失(正如顯示繪製第三幀的丟失)。然,這是不現實的,於是就有了會堵車!
同樣的,顯示器16.7ms重新整理間隔之前發生了其他繪製請求(setTimeout),導致所有第三幀丟失,繼而導致動畫斷續顯示(堵車的感覺),這就是過度繪製帶來的問題。不僅如此,這種計時器頻率的降低也會對電池使用壽命造成負面影響,並會降低其他應用的效能。(這裡是原文)
我比較認同下面這個觀點
‘setTimeout為10ms,為什麼我感覺應該丟失的是第二幀,第一幀10ms後進行繪製,此時瀏覽器剛開始進行渲染。然後過10ms,此時還沒達到瀏覽器的顯示頻率是16.7ms,所以第二幀丟失,再過10ms,距離第一次繪製已經過去20ms了大於16.7ms,所以第三幀可以繪製。
上面這點歧義也只是計算上有點問題,但是問題原作者已經給我們解釋得很清楚了。
過度繪製帶來的問題一是會出現掉幀的情況(丟失某一時間點的動畫)造成卡頓,二是大量的UI渲染任務聚集在後面會導致電池壽命及其他應用的效能。
而強制清空瀏覽器UI渲染佇列會引起大量的UI渲染任務聚集在一次EventLoop中(同步中)去完成,自然會導致上面第二點,大量的UI渲染任務聚集在後面會導致電池壽命及其他應用的效能。

5、減少迴流和重繪

5.1、批量修改DOM或者樣式

最核心的思想就是對於對引起迴流和重繪的操作不要一次一次的修改,應該用一個容器裝起來,一次性修改
比如修改樣式如下:

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

//可以優化為使用cssText或者class統一新增
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
//或者
const el = document.getElementById('test');
el.className += ' active';

再比如增加DOM節點:
考慮我們要執行一段批量插入節點的程式碼:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

如上面這種寫法會導致多次迴流
優化方式可以用document.createDocumentFragment

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

前面有說到渲染樹迴流、重繪都是針對可見元素,那麼就有以下優化方案
原理就是利用display:none隱藏元素,進行各種增刪改元素的操作,操作完再使其可見,對display:none隱藏元素進行操作是不會引起迴流的。下面程式碼只會在ul顯示的時候(display:block的時候)進行一次迴流。

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

5.2、避免觸發同步UI渲染

在前面瀏覽器優化那一章中,提到那些關於獲取節點(元素)佈局資訊的屬性不能過分使用,會引起瀏覽器的強制迴流或重繪,也就是在同步中進行迴流重繪,所以我們也需要減少對這類屬性的使用。
例子如下

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

這段程式碼看上去是沒有什麼問題,可是其實會造成很大的效能問題。在每次迴圈的時候,都讀取了box的一個offsetWidth屬性值,然後利用它來更新p標籤的width屬性。這就導致了每一次迴圈的時候,瀏覽器都必須先使上一次迴圈中的樣式更新操作生效,才能響應本次迴圈的樣式讀取操作。每一次迴圈都會強制瀏覽器重新整理佇列。我們可以優化為:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

5.3、對於複雜動畫效果,使用絕對定位讓其脫離文件流

對於複雜動畫效果,由於會經常的引起迴流重繪,因此,我們可以使用絕對定位,讓它脫離文件流。否則會引起父元素以及後續元素頻繁的迴流
可以看這個例子,可以開啟除錯面板檢視FPS的變化

例子中在未優化前,元素未絕對定位,依靠margin的變化進行動畫,引起大量的迴流,而當使用了絕對定位,明顯感覺FPS穩定在每秒60次,也就是16.6ms一次。

5.4、css3硬體加速(GPU加速)

比起考慮如何減少迴流重繪,我們更期望的是,根本不要回流重繪。這個時候,css3硬體加速就閃亮登場啦!!
劃重點:

  1. 使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪 。
  2. 對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能
    如何使用
    常見的觸發硬體加速的css屬性:
    transform
    opacity
    filters
    Will-change
    效果
    我們可以先看個例子。我通過使用chrome的Performance捕獲了動畫一段時間裡的迴流重繪情況,實際結果如下圖:

從圖中我們可以看出,在動畫進行的時候,沒有發生任何的迴流重繪。如果感興趣你也可以自己做下實驗。
重點
使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪
對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能。
css3硬體加速的坑
當然,任何美好的東西都是會有對應的代價的,過猶不及。css3硬體加速還是有坑的:
如果你為太多元素使用css3硬體加速,會導致記憶體佔用較大,會有效能問題。
在GPU渲染字型會導致抗鋸齒無效。這是因為GPU和CPU的演算法不同。因此如果你不在動畫結束的時候關閉硬體加速,會產生字型模糊。
參考文章連結:
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/24
https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%e5%8a%a8%e7%94%bb%e7%ae%97%e6%b3%