回流 重绘

657 阅读6分钟

先谈两点

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

回流的代价要远大于重绘;

回流

当 Render Tree 中部分或全部节点, 因元素的尺寸、布局、隐藏等改变而需要重新构建,计算它们在设备视口 (viewport) 内的确切位置和大小,浏览器重新渲染的过程称为 回流。

会导致回流的操作:

  • 页面首次渲染。
  • 浏览器窗口大小发生改变。
  • 元素尺寸或者位置发生改变。
  • 元素内容变化(文字数量或者图片大小发生改变)。
  • 元素字体大小的改变。
  • 添加或者删除可见的 DOM 元素。
  • 激活 CSS 伪类 (eg: :hover)。
  • 查询某些属性或调用某些方法。

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

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

重绘

当页面中元素样式(例如:colorbackground-color等)的改变并不影响它周围或内部布局(尺寸、大小等)时,浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

容易造成重绘操作的css

  • 颜色
  • 边框样式 border-style
  • 圆角 border-radius
  • 文本修饰 text-decoration
  • 阴影 box-shadow
  • 轮廓线 outline
  • 背景 background

注意

异步reflow

有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow 或 repaint 一次,浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一定的临界值,浏览器就会挨个执行,进行一次批处理,将队列清空,这样可以把多次回流和重绘变成一次,这又叫 异步reflow增量异步reflow

立即reflow

在有些情况下(获取布局信息操作的时候),会强制将队列清空,也就是强制回流,比如resize 窗口,改变了页面默认的字体等;

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

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

css3不需要回流和重绘的属性:

  • transform // 向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜
  • opacity // 设置元素的不透明级别
  • filter // 定义了元素(通常是)的可视效果(例如:模糊与饱和度)
  • Will-change // 提前通知浏览器我们要对元素做什么动画,这样浏览器可以提前准备合适的优化设置

浏览器绘制 DOM

1、获取 DOM 并将其分割为多个层(layer)
2、将每个层独立地绘制进位图(bitmap)中
3、将层作为纹理(texture)上传至 GPU
4、复合(composite)多个层来生成最终的屏幕图像。

通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上 传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

transform 不重绘,不回流 是因为transform属于合成属性,对合成属性进行transition/animate动画时,将会创建一个合成层(composite layer)。这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。浏览器会通过重新复合来创建动画帧。

优化 减少回流

没有定位变化,只显隐

尽量将元素设置为visibility ,因为display:none会引起回流,而visibility 只会引起重绘。

避免使用 table 布局

尽量不要使用table 布局。如果没有定宽,表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。

对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,尽量将元素设置为绝对定位absolute或者fixed,使得脱离文档流。操作元素的定位属性时,只有这一个元素会回流,不然容易引起父元素以及后续元素频繁的回流。

合并样式修改

减少造成回流的次数,如果要给一个节点操作多个css属性,而每一个都会造成回流的话,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性(而不是利用js修改单独每一个样式)。

批量操作DOM

当对DOM有多次操作的时候,需要使用一些特殊处理减少触发回流,在脱离标准流后,对元素进行的多次操作,不会触发回流。等操作完成后,再将元素放回标准流。

脱离标准流的操作有以下3种:

  1. 隐藏元素(display: none,只引发两次回流和重绘)
  2. 使用文档碎片(createDocumentFragment:创建一个虚拟的 documentFragment 节点)
  3. 拷贝节点

:下面对DOM节点的多次操作,每次都会触发回流

var data = [
    {
        id:1,
        name:"商品1",
    },
    {
        id:2,
        name:"商品1",
    },
    {
        id:3,
        name:"商品1",
    },
    {
        id:4,
        name:"商品1",
    },
    // 假设后面还有很多
];
var oUl = document.querySelector("ul");
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    oUl.appendChild(oLi);
}

这样每次给ul中新增一个li的操作,每次都会触发回流。

方法一:隐藏ul后,给ul添加节点,添加完成后再将ul显示

oUl.style.display = 'none';
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    oUl.appendChild(oLi);
}
oUl.style.display = 'block';

此时,在隐藏ul和显示ul的时候,触发了两次回流,给ul添加每个li的时候没有触发回流。

方法二:创建文档碎片,将所有li先放在文档碎片中,等都放进去以后,再将文档碎片放在ul中

var fragment = document.createDocumentFragment();
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    fragment.appendChild(oLi);
}
oUl.appendChild(fragment);

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象. 与 Document 最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的(重新渲染) ,且不会导致性能等问题。

使用 document.createDocumentFragment 方法或者构造函数来创建一个空的 DocumentFragment.

详细

方法三:将ul拷贝一份,将所有li放在拷贝中,等都放进去以后,使用拷贝(深拷贝)替换掉ul。

var newUL = oUl.cloneNode(true);
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    newUL.appendChild(oLi);
}
oUl.parentElement.replaceChild(newUl, oUl);

注意

cloneNode() 代表浅拷贝
—标签里的元素,事件没有复制。
cloneNode(true) 代表深拷贝
—完全把真节点的东西给复制了过来

避免多次触发布局

获取一个元素的scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight之类的属性,浏览器为了保证值的正确会回流取得最新的值,所以如果要多次操作,最好取完做个缓存。

如下回到顶部的操作:

goBack.onclick = function(){
    setInterval(function(){
        var t = document.documentElement.scrollTop || document.body.scrollTop;
        t += 10;
        document.documentElement.scrollTop = document.body.scrollTop = t;
    },20)
}

每隔20毫秒都会重新获取滚动过的距离,每次都会触发回流,代码优化如下:

goBack.onclick = function(){
    var t = document.documentElement.scrollTop || document.body.scrollTop;
    setInterval(function(){
        t += 10;
        document.documentElement.scrollTop = document.body.scrollTop = t;
    },20)
}

只获取一次,每次都让数字递增,避免每次都获取滚动过的距离。