reflow和repaint引起的性能问题

reflow和repaint在pc端只要不是怀有明知山有虎,偏向虎山行的心态写代码,这两货几乎不会引起性能问题, 可是移动端的渲染能力和pc端差了不止一个大截,一个不当心reflow和repaint就成了移动端的“性能杀手”。因此了解reflow和repaint也是颇有必要的,在考量页面性能的时候分析reflow和repaint也算是一个切入点。javascript

是什么


reflow 回流,或者叫重排均可以。回流(reflow)这个名词指的是浏览器为了从新渲染部分或所有的文档而从新计算文档中元素的位置和几何结构的过程。css

简单来讲就是当页面布局或者几何属性改变时就须要reflow。html

在一个页面中至少在页面刚加载的时候有一次reflow,在reflow的过程当中浏览器会将render tree中受影响的节点失效,再从新构建render tree,有时候,即便仅仅回流一个单一的元素,也可能要求它的父元素以及任何跟随它的元素也产生回流java

repaint重绘,当页面中的元素只须要更新样式风格不影响布局,好比更换背景色background-color,这个过程就是重绘。react

如何触发


reflow

从reflow的定义中就能够听出一些来,元素的布局和几何属性改变时就会触发reflow。主要有这些属性:程序员

  • 盒模型相关的属性: width,height,margin,display,border,etcweb

  • 定位属性及浮动相关的属性: top,position,float,etcchrome

  • 改变节点内部文字结构也会触发回流: text-align, overflow, font-size, line-height, vertival-align,etccanvas

除开这三大类的属性变更会触发reflow,如下状况也会触发:浏览器

  • 调整窗口大小
  • 样式表变更
  • 元素内容变化,尤为是输入控件
  • dom操做
  • css伪类激活
  • 计算元素的offsetWidth、offsetHeight、clientWidth、clientHeight、width、height、scrollTop、scrollHeight

repaint

页面中的元素更新样式风格相关的属性时就会触发重绘,如background,color,cursor,visibility,etc

注意:由页面的渲染过程可知,reflow必将会引发repaint,而repaint不必定会引发reflow

了解有哪些属性值改变会触发回流或者重绘点击这里

聪明的浏览器


设想一个这样的场景,咱们须要在一个循环中不断修改一个dom节点的位置或者是内容

document.addEventListener('DOMContentLoaded', function () {
    var date = new Date();
    for (var i = 0; i < 70000; i++) {
        var tmpNode = document.createElement("div");
        tmpNode.innerHTML = "test" + i;
        document.body.appendChild(tmpNode);
    }
    console.log(new Date() - date);
}); 
复制代码

这里屡次测量消耗时间大概在500ms(运行环境均为pc端,小霸王笔记本)。看到这个结果可能就有疑问了,这里有70000次内容的修改,就有70000reflow操做,也就用了500ms的时间(归功于迟缓的dom操做),说好的reflow消耗性能呢。

其实在这个过程当中,浏览器为了防止咱们犯二把屡次reflow操做放在循环中而引起浏览器假死,作了一个聪明的小动做。它会收集reflow操做到缓存队列中直到必定的规模或者过了特定的时间,再一次性地flush队列,反馈到render tree中,这样就将屡次的reflow操做减小为少许的reflow。可是这样的小动做带来了另一个问题,若是咱们想要在一次reflow事后就获取元素变更事后的值呢?这个时候浏览器为了获取真实的值就不得不当即flush缓存的队列。这些值或方法包括:

  • offsetTop/Left/Width/Height
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • getComputedStyle(), or currentStyle in IE

犯二代码以下:

document.addEventListener('DOMContentLoaded', function () {
            var date = new Date();
            for (var i = 0; i < 70000; i++) {
                var tmpNode = document.createElement("div");
                tmpNode.innerHTML = "test" + i;
                document.body.offsetHeight; // 获取body的真实值
                document.body.appendChild(tmpNode);
            }
            console.log("speed time", new Date() - date);
        });
复制代码

通常人应该不会去运行这种代码,若是你运行了的话,恭喜你的电脑-1s。可是若是没有衡量指标,优化性能也就无从谈起。

“If you cannot measure it, you cannot improve it.” -Lord Kelvin

为了防止浏览器假死,把循环次数都改成7000次,得出的结果是(屡次平均):

  • 获取了真实值的样例用时约18000ms
  • 没有获取真实值的样例用时约50ms

经过这两个样例印证了浏览器确实有优化reflow的小动做,聪明的程序员不会依赖浏览器的优化策略,在平常开发中遇到for循环就应该慎重编写循环体内部的代码。

减小reflow和repaint


如何减小reflow和repaint呢?回到定义去,reflow在页面布局或者定位发生变化时才会发生,从定义中咱们至少能够得出两个优化思路

  • 减小reflow操做
  • 替代会触发回流的属性

减小reflow操做

其本质上为减小对render tree的操做。render tree也就是渲染树,它的每一个节点都是可见,且包含该节点的内容和对应的规则样式,这也是render tree和dom数最大的区别所在, 减小reflow操做,主旨是合并多个reflow,最后再反馈到render tree中,诸如:

1,直接更改classname

// 很差的写法
    var left = 1;
    var top = 2;
    ele.style.left = left + "px";
    ele.style.top = top + "px";
    // 比较好的写法
    ele.className += " className1";
复制代码

或者直接修改cssText:

ele.style.cssText += ";
    left: " + left + "px;
    top: " + top + "px;";
复制代码

2.让频繁reflow的元素“离线”

  • 使用DocumentFragment进行缓存操做,引起一次回流和重绘;
  • 使用display:none,只引起两次回流和重绘;
  • 使用cloneNode(true or false) 和 replaceChild 技术,引起一次回流和重绘;

Dom规定文档片断(document fragment)是一种“轻量级”的文档,能够包含和控制节点,但不会想完整的文档那样占用额外的资源。虽然不能把文档片断直接添加到文档中,可是能够将它做为一个“仓库”来使用,便可以在里面保存未来可能会添加到文档中的节点。 好比最开始的样例结合DocumentFragment就能够这样写:

document.addEventListener('DOMContentLoaded', function () {
        var date = new Date(),
            fragment = document.createDocumentFragment();
        for (var i = 0; i < 7000; i++) {
            var tmpNode = document.createElement("div");
            tmpNode.innerHTML = "test" + i;
            fragment.appendChild(tmpNode);
        }
        document.body.appendChild(fragment);
        console.log("speed time", new Date() - date);
    });
复制代码

将多个修改结果收纳到了documentFragment这个“仓库”中,这个过程并不会影响到render tree,待循环完毕再将这个“仓库”的“存货”添加到dom上,以此达到减小reflow的目的,使用cloneNode也是同理。 而使用display:none来下降reflow的性能开销的原理在于使节点从render tree中失效,等通过多个会触发reflow操做后再“上线”

3.减小会flush缓存队列属性的访问次数,若是必定要访问,使用缓存

// 很差的写法
for(let i = 0; i < 20; i++ ) { 
    el.style.left = el.offsetLeft + 5 + "px"; 
    el.style.top = el.offsetTop + 5 + "px"; 
}
// 比较好的写法
var left = el.offsetLeft, 
top = el.offsetTop, 
s = el.style; 
for (let i = 0; i < 20; i++ ) { 
    left += 5;
    top += 5; 
    s.left = left + "px"; 
    s.top = top + "px"; 
}
复制代码

替代会触发reflow和repaint的属性

咱们能够将一些会触发回流的属性替换,来避免reflow。好比用translate代替top,用opacity替代visibility

样例代码:

<!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> #react { position: relative; top: 0; width: 100px; height: 100px; background-color: red; } </style>
</head>

<body>
    <div id="react"></div>
    <script type="text/javascript"> setTimeout(() => { document.getElementById("react").style.top = "100px" }, 2000); </script>
</body>
</html>
复制代码

代码很简单,页面上有一个红色的方块,2秒后它的top值将会变为“100px”,为了方便体现替代的属性能够避免reflow这里咱们使用chrome的开发者工具,部分截图以下

如上图,在top值变为“100px”的过程当中有上图五个阶段.

  • Recalculate Style,浏览器计算改变事后的样式
  • Layout,这个过程就是咱们说得reflow回流过程
  • Update Layer Tree,更新Layer Tree
  • Paint,图层的绘制过程
  • Composite Layers,合并多个图层

咱们把这五个过程用时记下:80 + Layout(73) + 72 + 20 + 69 = 316us

再用translate替代top:

-       position: relative;
-       top: 0;
+       transform: translateY(0);

-       document.getElementById("react").style.top = "100px"
+       document.getElementById("react").style.transform = "translateY(100px)"
复制代码

Performace截图:

能够看到用translate替换top后减小了原来的Layout也就是reflow的过程,用时:81 + 80 + 36 + 83 = 280us。 结果很是明显315us减小到了280us。有人说这个效果不明显呀,可是让咱们设想这样一个业务场景,有许多网站都会有不停移动的飘窗,这种飘窗一般是用定时器实现,每隔100ms就去修改一次它的top,若是用translate的话1s就能够减小10次reflow,若是这个飘窗样式比较多,比较复杂,那么1秒钟减小的10次reflow就有可能 减小几百毫秒甚至几秒Layout的过程

咱们再用opacity去替代visibility试试看。

-            document.getElementById("react").style.transform = "translateY(100px)"            
+            document.getElementById("react").style.visibility = "hidden"            
复制代码

Performace截图:

visibility属性值改变只会触发repaint,不会触发reflow,因此只有四个阶段,其中第三个阶段Paint就是重绘的体现,用时:48 + 50 + Paint(14) + 71 = 183us。咱们再用opacity替代visibility

+            opacity: 1;

-            document.getElementById("react").style.visibility = "hidden"    
+            document.getElementById("react").style.opacity = "0"
复制代码

按照上面的样例,应该得出用opacity替代visibility后重绘也就是Paint这个过程会消失从而达到性能提高的目的,既然这样咱们来看Performace截图:

对,你没有看错,我也没有截错图,此次不光是Paint过程没有消失,就连Layout都出现了,惊不惊喜!意不意外!
咱们再来重定义一下repaint重绘,它是从新绘制当前图层的内容,(什么是图层, 点击查看这篇文章)

其实opacity变化并不能改变这个图层的内容,改变的只是当前图层的alpha通道的值,从而来决定这个图层显不显示。可是这opacity变化的元素并非单独的图层,而是在document这个图层上的,以下Layers截图:

就是说浏览器并不能由于图层里面有一个opacity为0的元素就让整个图层的alpha通道变为零,而让整个图层不显示,因此就有了Layout和Paint这两个过程。解决办法也很简单那就是直接让这个元素单独为一个图层

修改css新建图层有两种办法:

  • will-change:transform
  • transform:translateZ(0)

这里咱们用下面一个

+   transform: translateZ(0);
复制代码

Performace截图:

如今就和理想中的状况同样了,用opacity替代visibility能够避免Paint重绘的过程。再来看看用时: 66 + 53 + 52 = 171us

这里因为我变更的元素很是简单,只有一个简单的div,减小Paint过程带来的优化收益并非很明显,若是是Paint过程是毫秒级别减小Paint过程的效果仍是可观的。

由上述两个替代会触发reflow和repaint的属性取得性能优化收益的例子中能够看出,这个方法是可行的,除开第一点减小reflow操做和第二点替换属性之外还有一些方法能够减小reflow和repaint

  • 减小table的使用

  • 动画实现的速度选择

  • 对于动画新建图层

    table自带的样式和一些很是方便的特性会方便咱们的开发,可是table也有一些与生俱来的性能缺陷,若是想要修改表格里无论哪个单元格,都会致使整张表格的从新Layout,若是这个表格很大,性能的消耗会有一个上升成本的。

图层的运用


在上一个样例中咱们新建了一个图层实现了opacity替代visibility去减小repaint的可行性,那么图层还有什么其余运用吗?答案是有的,咱们能够将一些频繁重绘回流的DOM元素做为一个图层,那么这个DOM元素的重绘和回流的影响只会在这个图层中,固然若是你为每个元素都建立一个图层那样确定也会聪明反被聪明误,还记得上述的Performance截图中的过程吗,最后一个Composite Layers这个过程就是合并多个图层的,图层过多这个过程会很是耗时,其实这个过程自己也很是耗时,原则上是在必要的状况下才会新建图层来减小重绘和回流的影响范围,到底使不使用就须要开发人员在业务情景中balance. 在Chrome浏览器下能够这样建立图层:

  • 3D或透视变换CSS属性(perspective transform)
  • 使用加速视屏解码的video标签
  • 拥有3D(WebGL)上下文或加速的2D上下文的canvas
  • 混合插件如(如Flash)
  • 对本身的opacity作CSS动画或使用一个动画webkit变换的元素
  • 拥有加速CSS过滤器的元素(GPU加速)
  • 元素有一个包含复合层的后代节点
  • 元素有一个z-index较低且包含一个复合层的兄弟元素
  • will-change: transform;

大致思路就是咱们把频繁重绘回流的DOM元素做为一个图层,那么这个DOM元素的重绘和回流的影响只会在这个图层中,来提高性能。举个栗子,咱们打开chrome开发者工具中的Layers,而后打开某网站

从红框中能够看出这个网站已经被分为了不少图层,当前选中的的这个baner图层在视图区域已经标注出来,由图可知,将一个常常触发回流和重绘的元素新开图层也算一个优化性能的作法。咱们再勾选这个选项

浏览器会用绿色高亮出当前正在repaint的元素,勾选上事后咱们打开一个视频:

能够看到视频在播放过程当中一直处于高亮状态,这个不难理解,video为单独一个图层,在整个视频播放过程当中video接受到发送过来的每一帧,都会将触发video所在图层的重绘。

结语

简单回顾一下本文,咱们最开始聊了一下reflow和repaint是什么,如何触发它们,接下来谈了一下浏览器在处理它们所采起的策略,最后就是如何避免reflow和repaint带来的性能开销,还补充了一下图层的存在乎义和简单运用。 其实在优化reflow和repaint上就是两点:

  • 避免使用触发reflow、repaint的css属性
  • 将reflow、repaint的影响范围限制在单独的图层以内

参考资料

https://csstriggers.com

http://blog.csdn.net/luoshengyang/article/details/50661553

相关文章
相关标签/搜索