迴流與重繪

迴流:當咱們對 DOM 的修改引起了 DOM 幾何尺寸的變化(好比修改元素的寬、高或隱藏元素等)時,瀏覽器須要從新計算元素的幾何屬性(其餘元素的幾何屬性和位置也會所以受到影響),而後再將計算的結果繪製出來。這個過程就是迴流(也叫重排)。html

重繪:當咱們對 DOM 的修改致使了樣式的變化、卻並未影響其幾何屬性(好比修改了顏色或背景色)時,瀏覽器不需從新計算元素的幾何屬性、直接爲該元素繪製新的樣式(跳過了上圖所示的迴流環節)。這個過程叫作重繪。瀏覽器

由此咱們能夠看出,重繪不必定致使迴流,迴流必定會致使重繪。硬要比較的話,迴流比重繪作的事情更多,帶來的開銷也更大。但這兩個說到底都是吃性能的,因此都不是什麼善茬。咱們在開發中,要從代碼層面出發,儘量把迴流和重繪的次數最小化。緩存

哪些實際操做會致使迴流與重繪

要避免迴流與重繪的發生,最直接的作法是避免掉可能會引起迴流與重繪的 DOM 操做,就好像拆彈專家在解決一顆炸彈時,最重要的是掐滅它的導火索。bash

觸發重繪的「導火索」比較好識別——只要是不觸發迴流,但又觸發了樣式改變的 DOM 操做,都會引發重繪,好比背景色、文字色、可見性(可見性這裏特指形如visibility: hidden這樣不改變元素位置和存在性的、單純針對可見性的操做,注意與display:none進行區分)等。爲此,咱們要着重理解一下那些可能觸發迴流的操做。工具

迴流的「導火索」

  • 最「貴」的操做:改變 DOM 元素的幾何屬性

這個改變幾乎能夠說是「牽一髮動全身」——當一個DOM元素的幾何屬性發生變化時,全部和它相關的節點(好比父子節點、兄弟節點等)的幾何屬性都須要進行從新計算,它會帶來巨大的計算量。佈局

常見的幾何屬性有 width、height、padding、margin、left、top、border 等等。此處再也不給你們一一列舉。有的文章喜歡羅列屬性表格,但我相信我今天列出來你們也不會看、看了也記不住(由於太多了)。我本身也不會去記這些——其實確實不必記,️一個屬性是否是幾何屬性、會不會致使空間佈局發生變化,你們寫樣式的時候徹底能夠經過代碼效果看出來。多說無益,還但願你們能夠多寫多試,造成本身的「肌肉記憶」。性能

  • 「價格適中」的操做:改變 DOM 樹的結構

這裏主要指的是節點的增減、移動等操做。瀏覽器引擎佈局的過程,順序上能夠類比於樹的前序遍歷——它是一個從上到下、從左到右的過程。一般在這個過程當中,當前元素不會再影響其前面已經遍歷過的元素。優化

  • 最容易被忽略的操做:獲取一些特定屬性的值

當你要用到像這樣的屬性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 時,你就要注意了!ui

「像這樣」的屬性,究竟是像什麼樣?——這些值有一個共性,就是須要經過即時計算獲得。所以瀏覽器爲了獲取這些值,也會進行迴流。編碼

除此以外,當咱們調用了 getComputedStyle 方法,或者 IE 裏的 currentStyle 時,也會觸發迴流。原理是同樣的,都爲求一個「即時性」和「準確性」。

如何規避迴流與重繪

瞭解了迴流與重繪的「導火索」,咱們就要儘可能規避它們。但不少時候,咱們不得不使用它們。當避無可避時,咱們就要學會更聰明地使用它們。

將「導火索」緩存起來,避免頻繁改動

有時咱們想要經過屢次計算獲得一個元素的佈局位置,咱們可能會這樣作:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    #el {
      width: 100px;
      height: 100px;
      background-color: yellow;
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="el"></div>
  <script>
  // 獲取el元素
  const el = document.getElementById('el')
  // 這裏循環斷定比較簡單,實際中或許會拓展出比較複雜的斷定需求
  for(let i=0;i<10;i++) {
      el.style.top  = el.offsetTop  + 10 + "px";
      el.style.left = el.offsetLeft + 10 + "px";
  }
  </script>
</body>
</html>

複製代碼

這樣作,每次循環都須要獲取屢次「敏感屬性」,是比較糟糕的。咱們能夠將其以 JS 變量的形式緩存起來,待計算完畢再提交給瀏覽器發出重計算請求:

// 緩存offsetLeft與offsetTop的值
const el = document.getElementById('el') 
let offLeft = el.offsetLeft, offTop = el.offsetTop

// 在JS層面進行計算
for(let i=0;i<10;i++) {
  offLeft += 10
  offTop  += 10
}

// 一次性將計算結果應用到DOM上
el.style.left = offLeft + "px"
el.style.top = offTop  + "px"

複製代碼

避免逐條改變樣式,使用類名去合併樣式

好比咱們能夠把這段單純的代碼:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

複製代碼

優化成一個有 class 加持的樣子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .basic_style {
      width: 100px;
      height: 200px;
      border: 10px solid red;
      color: red;
    }
  </style>
</head>
<body>
  <div id="container"></div>
  <script>
  const container = document.getElementById('container')
  container.classList.add('basic_style')
  </script>
</body>
</html>

複製代碼

前者每次單獨操做,都去觸發一次渲染樹更改,從而致使相應的迴流與重繪過程。

合併以後,等於咱們將全部的更改一次性發出,用一個 style 請求解決掉了。

將 DOM 「離線」

咱們上文所說的迴流和重繪,都是在「該元素位於頁面上」的前提下會發生的。一旦咱們給元素設置 display: none,將其從頁面上「拿掉」,那麼咱們的後續操做,將沒法觸發迴流與重繪——這個將元素「拿掉」的操做,就叫作 DOM 離線化。

仍以咱們上文的代碼片斷爲例:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了許多相似的後續操做)

複製代碼

離線化後就是這樣:

let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了許多相似的後續操做)
container.style.display = 'block'

複製代碼

有的同窗會問,拿掉一個元素再把它放回去,這不也會觸發一次昂貴的迴流嗎?這話不假,但咱們把它拿下來了,後續無論我操做這個元素多少次,每一步的操做成本都會很是低。當咱們只須要進行不多的 DOM 操做時,DOM 離線化的優越性確實不太明顯。一旦操做頻繁起來,這「拿掉」和「放回」的開銷都將會是很是值得的。

Flush 隊列:瀏覽器並無那麼簡單

以咱們如今的知識基礎,理解上面的優化操做並不難。那麼如今我問你們一個問題:

let container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

複製代碼

這段代碼裏,瀏覽器進行了多少次的迴流或重繪呢?

「width、height、border是幾何屬性,各觸發一次迴流;color只形成外觀的變化,會觸發一次重繪。」——若是你馬上這麼想了,說明你是個能力不錯的同窗,認真閱讀了前面的內容。那麼咱們如今馬上跑一跑這段代碼,看看瀏覽器怎麼說:

這裏爲你們截取有「Layout」和「Paint」出鏡的片斷(這個圖是經過 Chrome 的 Performance 面板獲得的,後面會教你們用這個東西)。咱們看到瀏覽器只進行了一次迴流和一次重繪——和咱們想的不同啊,爲啥呢?

由於現代瀏覽器是很聰明的。瀏覽器本身也清楚,若是每次 DOM 操做都即時地反饋一次迴流或重繪,那麼性能上來講是扛不住的。因而它本身緩存了一個 flush 隊列,把咱們觸發的迴流與重繪任務都塞進去,待到隊列裏的任務多起來、或者達到了必定的時間間隔,或者「不得已」的時候,再將這些任務一口氣出隊。所以咱們看到,上面就算咱們進行了 4 次 DOM 更改,也只觸發了一次 Layout 和一次 Paint。

你們這裏尤爲當心這個「不得已」的時候。前面咱們在介紹迴流的「導火索」的時候,提到過有一類屬性很特別,它們有很強的「即時性」。當咱們訪問這些屬性時,瀏覽器會爲了得到此時此刻的、最準確的屬性值,而提早將 flush 隊列的任務出隊——這就是所謂的「不得已」時刻。具體是哪些屬性值,咱們已經在「最容易被忽略的操做」這個小模塊介紹過了,此處再也不贅述。

小結

整個一節讀下來,可能會有同窗感到疑惑:既然瀏覽器已經爲咱們作了批處理優化,爲何咱們還要本身操心這麼多事情呢?今天避免這個明天避免那個,多麻煩!

問題在於,並非全部的瀏覽器都是聰明的。咱們剛剛的性能圖表,是 Chrome 的開發者工具呈現給咱們的。Chrome 裏行得通的東西,到了別處(好比 IE)就不必定行得通了。而咱們並不知道用戶會使用什麼樣的瀏覽器。若是不手動作優化,那麼一個頁面在不一樣的環境下就會呈現不一樣的性能效果,這對咱們、對用戶都是不利的。所以,養成良好的編碼習慣、從根源上解決問題,仍然是最周全的方法。

相關文章
相關標籤/搜索