瀏覽器的迴流和重繪

瀏覽器的渲染過程

clipboard.png

從上面這個圖上,咱們能夠看到,瀏覽器渲染過程以下javascript

  1. 解析HTML生成DOM樹,解析CSS生成CSSOM
  2. DOM樹和CSSOM樹結合生成渲染樹renderTree
  3. Layout(迴流): 根據生成的渲染樹,進行迴流(Layout),獲得節點的幾何信息(位置,大小)
  4. Painting(重繪): 根據渲染樹以及迴流獲得的幾何信息,獲得節點的絕對像素
  5. Display:將像素髮送給GPU,展現在頁面上。

生成渲染樹(RenderTree)

clipboard.png

爲了構建渲染樹,瀏覽器主要完成了如下工做css

  1. DOM樹的根節點開始遍歷每一個可見節點。
  2. 對於每一個可見的節點,找到CSSOM樹中對應的規則,並應用它們。
  3. 根據每一個可見節點以及其對應的樣式,組合生成渲染樹。

第一步中,既然說到了要遍歷可見的節點,那麼咱們得先知道,什麼節點是不可見的。不可見的節點包括:html

  1. 一些不會渲染輸出的節點,好比script、meta、link等。
  2. 一些經過css進行隱藏的節點。好比display:none。注意,利用visibilityopacity隱藏的節點,仍是會顯示在渲染樹上的。只有display:none的節點纔不會顯示在渲染樹上。

迴流(Layout)

前面咱們經過構造渲染樹,咱們將可見DOM節點以及它對應的樣式結合起來,但是咱們還須要計算它們在設備視口(viewport)內的確切位置和大小,這個計算的階段就是迴流。
爲了弄清每一個對象在網站上的確切大小和位置,瀏覽器從渲染樹的根節點開始遍歷,而在迴流這個階段,咱們就須要根據視口具體的寬度,將其轉爲實際的像素值java

重繪(Painting)

經過迴流(Layout)階段,咱們知道了全部的可見節點的樣式和具體的幾何信息(位置、大小),那麼咱們就能夠將渲染樹的每一個節點都轉換爲屏幕上的實際像素,這個階段就叫作重繪節點。canvas

什麼時候發生迴流重繪

迴流階段是計算節點的幾何信息和位置,那麼當頁面佈局或者幾何信息發生改變時,就須要迴流。瀏覽器

  1. 添加或者刪除可見的DOM元素
  2. 元素的位置、尺寸發生變化
  3. 頁面開始渲染的時候(這確定避免不了)
  4. 瀏覽器的視口尺寸大小發生改變(由於迴流是根據瀏覽器視口的大小來計算元素的位置和尺寸大小)

注意:迴流必定會觸發重繪,而重繪(非幾何信息的樣式發生改變)不必定會迴流, reflow迴流的成本開銷要高於repaint重繪,一個節點的迴流每每回致使子節點以及同級節點的迴流;緩存

根據改變的範圍和程度,渲染樹中或大或小的部分須要從新計算,有些改變會觸發整個頁面的重排,好比,滾動條出現的時候或者修改了根節點。app

基於迴流(Layout)、重繪(Painting)的優化方法

避免擾亂現代瀏覽器的優化機制

在現代瀏覽器的中,因爲每次迴流、重繪的時候,都須要額外的計算消耗,所以會經過隊列化修改,並批量執行來優化這一過程。瀏覽器會將修改操做放入隊列裏面,直到過了一段時間或者達到一個閾值,才清空隊列。async

可是當你獲取佈局信息時,會強制刷新隊列,例如:ide

offsetTop、offsetLeft、offsetWidth、offsetHeight

scrollTop、scrollLeft、scrollWidth、scrollHeight

clientTop、clientLeft、clientWidth、clientHeight

getComputedStyle()

getBoundingClientRect()

上面這些方法,都須要獲取最新的佈局信息,因此瀏覽器會強制刷新隊列並執行迴流、重繪,來獲取最新的信息。
所以咱們在修改樣式的時候,應該儘可能避免使用上面的屬性、方法,若是非要使用,能夠先緩存起來而後一塊兒獲取。

CSS的修改方式

考慮如下代碼

const el = document.getElementById('el')
el.style.padding = 'xxx'
el.style.margin = 'xxx'
el.style.border = 'xxx'

這裏元素的幾何信息有三次被修改了,可是現代瀏覽器會將起緩存起來,可是若是這期間有經過前面列出來的屬性、方法訪問位置信息的話就會觸發三次迴流、重繪。因此仍是建議經過cssText或者class的方法一次性修改。

el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
// 或者
el.className += 'xxx';

批量修改DOM

當咱們須要對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');
ul.style.display = 'none'; // 首先脫離文檔流
appendDataToElement(ul, data);
ul.style.display = 'block'; // 操做完之後再可見
  • 使用文檔片斷(document fragment)在當前DOM以外構建一個子樹,再把它拷貝迴文檔。
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment()
appendDataToElement(fragment , data);
ul.appendChild(fragment )

獨立圖層

一個圖層的迴流和重繪只會在該圖層當中進行,不會影響其餘圖層,因此有必要的時候,能夠將某些元素放到單獨的圖層。

例如對於複雜動畫效果,因爲會常常的引發迴流重繪,所以,咱們可使用絕對定位,讓它脫離文檔流, 成爲一個單獨的圖層。不然會引發父元素以及後續元素頻繁的迴流。可是因該儘可能少許使用圖層,由於圖層的合成是特別消耗性能,一個頁面當中不能有過多的圖層, 在使用了圖層以後須要進行先後對比

會自動創建圖層的狀況:

  • 3d或者透視變換、過渡css屬性
  • 使用<video>節點
  • <canvas>
  • flash
  • 多透明度作 css動畫

其餘優化

  • 用translate替代top改變: top會觸發迴流,而前者不會
  • 用opacity替代visibility: 前者回流重繪都不會觸發(前提是它單獨在一個圖層),後者兩個都會觸發
  • 不要使用table佈局,table的可能很小的一個改動會形成迴流,很影響性能,應該儘可能使用 div。
  • 動畫實現的速度選擇:
  • 對於動畫新建圖層
  • 啓用GPU硬件加速: 使用transform:translateZ(0) 、transform:translate3d(0,0,0)來開啓GPU硬件加速

CSSJS 是這樣阻塞 DOM 解析和渲染的

經過<script><link>引入外部資源,當解析到該標籤的時候,會進行下載。

  1. CSS腳本的加載不會阻塞 DOM 解析過程,可是會阻塞渲染過程(painting)
  2. JS腳本的加載與執行會阻塞 DOM 解析過程, 可是不會阻塞後續資源的加載
  3. JS腳本的加載中,若是你肯定不必阻塞 DOM 解析的話,不妨按須要加上 defer 或者 async 屬性,此時腳本下載的過程當中是不會阻塞 DOM 解析的。
  4. 瀏覽器遇到 <script>且沒有 deferasync 屬性的標籤時,爲了爲<script>標籤內部的js提供最新的信息,會觸發頁面的迴流、重繪過程。
  5. 若是前面 CSS 資源還沒有加載完畢時,瀏覽器會等待它加載完畢以後再執行腳本。即 css 不阻塞 js 的加載,但阻塞它的執行。

因此<script>最好放底部(防止阻塞DOM解析)。<link>最好放頭部(爲渲染過程提供樣式)。若是頭部同時有<script><link>的狀況下,最好將<script>放在<link>上面(爲了防止CSS腳本加載時間過長,使js等待時間也很長)dd

deferasync

直接看圖吧,綠色的表明 html 解析,藍色的表明 javascript 腳本的下載,紅色的表明 javaScript 腳本的執行。

bVcQV0

相關文章
相關標籤/搜索