整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CSS-重绘 回流(Reflow &

CSS-重绘 回流(Reflow & Repaint)

流必将引起重绘,重绘不一定会引起回流。

浏览器会把HTML解析成DOM,把CSS解析成CSSOM CSS Object Model即 CSSOM 和DOM合并就产生了Render Tree

当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流

会导致回流的操作:

页面首次渲染

浏览器窗口大小发生改变

元素尺寸或位置发生改变

元素内容变化(文字数量或图片大小等等)

元素字体大小变化

添加或者删除可见的DOM元素

激活CSS伪类(例如::hover)

查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

clientWidth、clientHeight、clientTop、clientLeft

offsetWidth、offsetHeight、offsetTop、offsetLeft

scrollWidth、scrollHeight、scrollTop、scrollLeft

scrollIntoView()、scrollIntoViewIfNeeded()

getComputedStyle()

getBoundingClientRect()

scrollTo()

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

!!!回流比重绘的代价要更高

当你访问以下属性或方法时,浏览器会立刻清空队列:

clientWidth、clientHeight、clientTop、clientLeft

offsetWidth、offsetHeight、offsetTop、offsetLeft

scrollWidth、scrollHeight、scrollTop、scrollLeft

width、height

getComputedStyle()

getBoundingClientRect()

如何避免

CSS

避免使用table布局。

尽可能在DOM树的最末端改变class。

避免设置多层内联样式。

将动画效果应用到position属性为absolute或fixed的元素上。

避免使用CSS表达式(例如:calc())。

JavaScript

避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。

避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。

也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流

能优化中,减少重绘重排应该是一种很好的优化方式,我们具体看一下什么情况下会造成重绘重排,为什么减少重绘重排可以做到优化,怎么样减少重绘重排。

浏览器渲染过程

我们先看看当浏览器拿到服务端返回的资源时,是如何渲染的。

首先浏览器会进行文件解析,主要解析三个东西:

  1. 解析 html/xhtml/svg,形成 dom 树。
  2. 解析 css,产生 CSS Rule Tree。
  3. 解析 js,js 会通过 api(包括 DOM API 和 CSSOM API) 来操作 Dom Tree 和 CSS Rule Tree。

解析完成之后

  1. 通过 CSS Rule Tree 和 Dom Tree 生成 rendering Tree。其中不包括 display:none 的元素。
  2. 计算 DOM 节点的位置,对元素进行分层及布局,也叫 reflow 和 layout 过程。

布局完成之后,就要进行绘制了,将各层发给 GPU,GPU 将各层合成,显示在屏幕上,即 composite。

什么情况下会造成重绘重排

当我们开始绘制的时候,如果使用 js 操作了 dom 元素,或者改变了 css 属性,就可能会造成重绘(repaint)和重排(reflow)。

repaint:屏幕的一部分进行了重画,比如某个 css 中改变背景色,元素尺寸没有变。 reflow:任何一个元素的尺寸发生了变化,需要重新验证并计算 render tree,就会造成重排。

在 PC 时代,我们用 jquery 进行获取元素,改变元素的尺寸,及时发生重排,我们也很难感知到,但是当移动时代到来之后,如果频繁发生重排,那手机就会受不了了。

尤其是在执行下面操作时,成本会很高:

  1. js 添加或者删除元素的时候
  2. js 改变元素的位置发生改变时
  3. 页面初始化
  4. 容器尺寸发生变化。比如在标准盒模型下添加 padding,border 就会造成重排,所以在书写样式的时候,很多会将盒模型设计成怪异盒模型(box-sizing: border-box)
  5. js 获取元素的尺寸单位。
  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • IE 中的 getComputedStyle(), 或 currentStyle

如果发生上述的行为基本都会造成重绘和重排。

所以,当发生重排时,一定会发生重绘,但是发生重绘不一定会发生重排。

浏览器中每个元素节点都有 reflow 方法,当一个元素发生 reflow 时,他的子节点都会发生 reflow。

举几个例子来说明一下造成重绘重排的情况:

var bodyStyle=document.body.style; // cache
bodyStyle.padding='20px'; // reflow, repaint
bodyStyle.border='10px solid red'; // 再一次的 reflow 和 repaint
bodyStyle.color='blue'; // repaint
bodyStyle.backgroundColor='#fad'; // repaint
bodyStyle.fontSize='2em'; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('children!'));

为什么减少重绘重排可以优化性能

前面说了浏览器的渲染机制,多一次重绘就需要浏览器重新进行一次绘制,及时 GPU 处理会比较快,但是也是吃不消的,更别说重排了,重排一个 dom,会重新生成 render Tree,然后重新绘制。

如何减少重绘重排

其实浏览器很聪明,不可能每次修改样式就 reflow 或者 repaint 一次,一般来说,浏览器会积累一批操作,然后做一次 reflow。

但是也有些例外情况,比如 resize 窗口,改变窗口字体,浏览器会立即进行 reflow。

虽然浏览器会这么做,但是我们也应该减少重绘重排的次数,在开发阶段就为浏览器进行特殊的关爱,毕竟是每天陪伴我们的小伙伴。

下面总结了一些针对 reflow 和 repaint 的最佳实践:

  1. 不要把 DOM 元素的属性值放到一个循环中当成循环的变量。
  2. 尽可能的修改低层级的节点,避免修改高层级的节点,造成大面积的 reflow。
  3. 千万不要使用 table 布局,修改一小块地方,会造成整个 table 重新布局。
  4. 动画尽量使用 css 动画,css 动画中尽量只使用 transform 和 opacity,这不会发生重排和重绘
  5. 不要一条一条的修改 DOM 样式,尽量提前设置好 class,后续增加 class,进行批量修改。
  • 把 DOM 离线后修改。使用 documentFragment 对象在内存里操作 DOM。
  • 使用 requestAnimationFrame 可以进行优化,在下一帧进行操作。
  • 把修改频繁的元素先 display: none,修改完之后显示,修改个 100 次也无妨。
  • clone 一个 dom 节点在内存里,修改之后;与在线的节点相替换。

composite

当每次布局完成之后,就会发生 composite 过程,浏览器都把重绘后的图像发给 GPU 去合成并显示。

在上面最佳实践中最后提到了动画,动画其实是比较耗费性能的,因为动画的每一帧都会发给 GPU 去合成,重绘重排会发生在动画的每一帧。

我们在写动画的时候,可以通过 js 写,也可以通过 css 写。两种方式在写动画时,过程也是不一样的。

  1. js 写的动画,过程:js 计算 -> 重排(若布局改变) -> 重绘 -> 合成
  2. css 动画,过程:重排(若布局改变)-> 重绘 -> 合成

所以不难看出,耗费性能最少并能并最流畅的动画是只触发合成。

为了仅发生 composite,我们做动画的 css property 必须满足以下三个条件:

  1. 不影响文档流。
  2. 不依赖文档流。
  3. 不会造成重绘。

满足以上以上条件的 css property 只有 transform 和 opacity。

这样的话,由于没有重排和重绘,只有合成,那么浏览器在动画执行之前就知道动画如何开始和结束。

并且有两个优势:

  1. 动画将会非常流畅
  2. 动画不在绑定到 CPU,即使 js 执行大量的工作;动画依然流畅

事实上影响动画流畅性的因素不止重排重绘,还有 CPU 内存。

css 动画有一个重要的特性,它是完全工作在 GPU 上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给 GPU。

而如果使用 js 动画,浏览器必须计算每一帧的状态;为了保证平滑的动画,我们必须在浏览器主线程计算新状态;把它们发送给 GPU 至少 60 次每秒。

除了计算和发送数据比 css 动画要慢,主线程的负载也会影响动画; 当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以最佳实践中最后一条就提到了,在写动画时,尽量写 css 动画,并且尽量用 transform 和 opacity。

辅助工具

谷歌浏览器检测重绘工具:右上角三点->更多工具->开发者工具->Performance。

chrome浏览器的Performance是页面性能分析的利器,网上有很多关于关于如何去使用和查看Performance的文章,这里就不多做阐述了,大伙可以多去了解了解。

小结

总之,页面性能优化是前端从初级到高级都避不开的一个话题,如何做到性能的最优化更是一个资深前端应该考虑的事情,这里也希望有更好更多见解的小伙伴能够私聊我,给我点意见。

文分享自华为云社区《前端页面之“回流重绘”-云社区-华为云》,作者:CoderBin。

“回流重绘”是什么?

HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:

  • 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置
  • 重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制

具体的浏览器解析渲染机制如下所示:

  • 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上

在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变

当我们对 DOM 的修改引发了 DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来

当我们对 DOM的修改导致了样式的变化(colorbackground-color),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了回流

如何触发

要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的

回流触发时机

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染的时候(这避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

还有一些容易被忽略的操作:获取一些特定属性的值

offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流

除此还包括getComputedStyle方法,原理是一样的

重绘触发时机

触发回流一定会触发重绘

可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)

除此之外还有一些其他引起重绘行为:

  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列

当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据

因此浏览器不得不清空队列,触发回流重绘来返回正确的值

三、如何减少

我们了解了如何触发回流和重绘的场景,下面给出避免回流的经验:

  • 如果想设定元素的样式,通过改变元素的 class 类名 (尽可能在 DOM 树的最里层)
  • 避免设置多项内联样式
  • 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值(如前文示例所提)
  • 避免使用 table 布局,table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算
  • 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响
  • 使用css3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘
  • 避免使用 CSS 的 JavaScript 表达式

在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能

但有时候,我们会无可避免地进行回流或者重绘,我们可以更好使用它们

例如,多次修改一个把元素布局的时候,我们很可能会如下操作

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";
}

每次循环都需要获取多次offset属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求

// 缓存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'

使用类名去合并样式

<style>
  .basic_style {
      width: 100px;
      height: 200px;
      border: 10px solid red;
      color: red;
  }
</style>
<script>
  const container=document.getElementById('container')
  container.classList.add('basic_style')
</script>

前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),

都去触发一次渲染树更改,从而导致相应的回流与重绘过程

合并之后,等于我们将所有的更改一次性发出

我们还可以通过通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作

点击下方,第一时间了解华为云新鲜技术~

华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云