回流与重绘

回流:当咱们对 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)就不必定行得通了。而咱们并不知道用户会使用什么样的浏览器。若是不手动作优化,那么一个页面在不一样的环境下就会呈现不一样的性能效果,这对咱们、对用户都是不利的。所以,养成良好的编码习惯、从根源上解决问题,仍然是最周全的方法。

相关文章
相关标签/搜索