浏览器渲染:重排和重绘【待整理】

@一棵菜菜  May 15, 2019

说明

今天大概看了下关于页面重排和重绘的两篇文章,觉得写得挺不错的,就转过来方便以后回顾啦。


网页生成过程.png

网页生成的时候,至少会渲染一次。用户访问的过程中,还会不断重新渲染。以下三种情况,会导致网页重新渲染。

  • 修改DOM
  • 修改CSS样式表
  • 用户事件(比如鼠标悬停、页面滚动、输入框键入文字、改变窗口大小等等)

重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow),后者叫做"重绘"(repaint)。

1. 概念

  • 重排(relayout) / 回流(reflow)——当Render Tree(渲染树)中的一部分(或全部)由于元素的尺寸、位置、隐藏(display:none/block)等改变而需要重新构建,浏览器为了重新渲染部分或整个页面,就会重新计算页面元素位置几何结构的过程。

    回流(reflow)——Gecko中布局的称谓,同时也是重排的别称。
    每个页面至少需要一次回流,就是在页面第一次加载的时候。
  • 重绘(replaint)——当页面中的元素只是外观风格被改变但不影响布局,比如更换背景色background-color、字体颜色color,这个过程就是重绘。

"重排/回流"必定会导致"重绘","重绘"不一定需要"重排"

比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。但比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。
重排所需的成本比重绘高的多。一个结点的重排很有可能导致子结点、甚至父点以及同级结点的重排。

形象举例

重排:这里我们可以理解为有很多人在排队,大家紧紧的依靠在一起,那什么时候大家需要都挪动下位置呢?我觉得要么就是一个人或者几个人突然变胖了/瘦了(尺寸变化),那大家如果想要继续依靠在一起,就得都动一动;或者其中一个人或者几个人挪动或交换了一下自己的位置,他势必也会挤着其他人去动一动位置(位置变化);或者一个定宽定高的盒模型里,某个或某些元素尺寸或位置发生了变化,那么只需要这个盒模型内部的元素动一动,不影响外部其他元素(局部变化)。这种重新渲染全部或部分文档的动作就是重排,因为大家都需要挪动下位置,也就导致我们这个网页需要回炉重造(回流)。

重绘:当队伍中的一个人需要换一件衣服,比如他从穿黄衣服换成穿红色的衣服,这个时候只要这一个人换件衣服就行了,对其他人并没有影响,这种情况我们就叫做重绘。浏览器只需要对该元素进行重新绘制即可。


2. 触发 重排/回流 的因素

重绘和重排的关系:在重排的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。

元素的布局和几何属性改变时就会触发reflow。主要有这些属性:

  • 页面渲染初始化(无法避免,此次所有组件都要进行首次布局,这是开销最大的一次重排)
  • 添加/删除可见DOM元素
  • 改变元素位置 ----- 定位属性及浮动

    position,top,right,bottom,left;float,clear
  • 改变元素尺寸(宽、高、内外边距、边框等) ----- 盒子模型的相关属性

    width,height,padding,border-width,margin,display,min-height
  • 改变元素内容(文本或图片等)

    text-align,line-height,vertival-align,overflow,font-size,font-family,font-weight 等
  • 改变浏览器窗口尺寸(resize事件发生时)

    如手机上的设备方向更改;浏览器窗口大小调节;
    文档视图调整大小时会触发 resize 事件:window.addEventListener('resize', function(){console.log('resized')});
  • 获取元素的某些属性,如:offsetWidth、offsetHeight、clientWidth、clientHeight、width、height、scrollTop、scrollHeight,请求了 getComputedStyle(), 或者 IE的 currentStyle。
注意:translate不会触发重排。

3. 触发 repaint(重绘) 的因素

页面中的元素更新外观风格相关的属性时就会触发重绘,如:visibility,background,color, border-style ,border-radius outline-color,cursor,text-decoration, box-shadow

例子:

$('body').css('color', 'red'); // repaint
$('body').css('margin', '2px'); // reflow, repaint

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; //  再一次的 reflow 和 repaint

bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

三、对于性能的影响

重排和重绘会不断触发,这是不可避免的。但是,它们非常耗费资源,是导致网页性能低下的根本原因

提高网页性能,就是要降低"重排"和"重绘"的频率和成本,尽量少触发重新渲染。

浏览器对重排的优化

因为,重排重绘花销很大,所以大部分浏览器都会对它们会进行优化:浏览器会维护1个队列,把所有会引起重排、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔(即定时定量),浏览器就会flush(冲洗)队列,进行一个批处理。这样就会让多次的重排、重绘变成一次重排重绘。

有些代码会强制浏览器提前flush队列

虽然有了浏览器的优化,但有些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器读取一些 style 信息的时候,就会让浏览器flush队列,比如:

offsetTop/offsetLeft/offsetWidth/offsetHeight
scrollTop/scrollLeft/scrollWidth/scrollHeight
clientTop/clientLeft/clientWidth/clientHeight
getComputedStyle(),或者 IE的 currentStyle
width,height
当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
// div元素有两个样式变动,但是浏览器只会触发一次重排和重绘:
div.style.color = 'blue';
div.style.marginTop = '30px';

// 如果写得不好,就会触发两次重排和重绘:
// 下面代码对div元素设置背景色以后,第二行要求浏览器给出该元素的位置,所以浏览器不得不立即重排
div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';

拓展

js获取浏览器窗口信息.png

参考:《JS 获取浏览器窗口大小clientWidth、offsetWidth、scrollWidth》
参考:《js获取浏览器窗口信息》
参考《JS中的位置和宽度》

5. 我们对重排的优化

减少重排、重绘其实就是需要减少对render tree的操作(合并多次DOM和样式的修改),并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:

(1)不要一条条地改变样式,而要通过改变class,或者js的cssText属性,一次性地改变样式。

cssText:
优点:可以尽量避免页面重排,提高页面性能;
缺点:会把原有的cssText清掉,为了解决这个问题,可以采用cssText累加的方法。

.newClassName {left:10px;top:10px}
var el = document.getElementById(“id”);

// bad 通过JS来覆写对象的样式是比较典型的一种销毁原样式并重建的过程,这种销毁和重建,都会增加浏览器的开销。
el.style.left = "10px";
el.style.top = "10px";

// good
el.className += " newClassName";
//or
el.style.cssText += ";left:10px;top:10px;width:20px;height:20px;"

(2)不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存从性能角度考虑,尽量不要把读操作和写操作,放在一个语句里面。

// bad
for (var i = 0; i < len; i++) {
  el.style.left = el.offsetLeft + x + "px";
}
// good
var x = el.offsetLeft,
for (var i = 0; i < len; i++) {
  x += 10;
  el.style = x + "px";
}


// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// good
var left = div.offsetLeft;
var top  = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";

(3)DOM离线化:使用display:none,只会引发两次重排和重绘

先将元素设为display: none(需要1次重排和重绘),则元素就不会存在于渲染树中,然后即使对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。这叫做DOM的离线化。

(4)position属性为absolutefixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响【要理解】

如过不使用,则会引起父元素及后续元素频繁重排重绘。

绝对布局虽然脱离了文档流,但不会创建新的复合图层!因此当绝对布局改变时(如修改top,left等),不会影响普通文档流的 render tree,但是 render tree 中它自身还是需要重排的,而且依然会绘制整个默认复合图层,对普通文档流是有影响的。
所以当修改该绝对定位的元素的 top 或 left 等属性时,还是会引起重排(局部自身)、重绘(整个默认复合图层)【待确认】。

(5)只在必要的时候,才将元素的display属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden的元素只对重绘有影响,不影响重排。

(6)不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局!!!

table 需要大量的 render tree 布局时间计算

(7)优化动画

  • 可以把动画效果应用到position属性为absolutefixed的元素上,这样对其他元素影响较小。
  • 动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
  • 动画实现的速度的选择,动画速度越快,重排次数越多,也可以选择使用 window.requestAnimationFrame()window.requestIdleCallback() 这两个方法调节重新渲染。
  • 开启 GPU 硬件加速

(8)CSS 选择符从右往左匹配查找,避免节点层级过多。

body > div > p > span

(9) GPU 硬件加速:开启新的复合图层【如下】【要理解】


图层【待整理】

什么是普通图层和复合图层?

可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层。

普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)。注意:绝对定位布局(absolutefixed),虽然脱离了普通文档流,但它仍然属于默认复合层。

GPU 进程可以开启一个新的复合图层(即硬件加速方式),它会单独分配资源,不会影响默认复合图层(普通文档流。所以不管这个复合图层中怎么变化,也不会影响默认复合层里的重排重绘),所以并不会影响周边的 DOM 结构,而属性的改变也会交给 GPU 处理,不会进行重排。

你可以想象成新的复合图层和默认复合图层是两幅画,相互独立,不会彼此影响。

可以简单理解为:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒。

Chrome中满足以下任意情况就会创建复合图层(硬件加速):

  • CSS3 过渡动画(transition),CSS3 3D变换(transform)
  • 使用<video>(加速视频解码) 、<canvas> <iframe> <webgl>等元素标签
  • opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • 拥有加速CSS过滤器的元素
  • 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
  • 混合插件(如 Flash)
  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),

使用translateopacity都不会触发重排和重绘,translate 是因为 GPU 进程会为对应 DOM 节点生成一个新的复合图层(也叫开启硬件加速),则 translate 样式变化会移交 GPU处理。

使用css3 的 translate 不会引起重排,因为 GPU 进程会为其开启一个新的复合图层,不会影响默认复合图层(就是普通文档流),所以并不会影响周边的 DOM 结构,而属性的改变也会交给 GPU 处理,不会进行重排。使 GPU 进程开启一个新的复合图层的方式还有 3D 动画,过渡动画,以及 opacity 属性,还有一些标签,这些都可以创建新的复合图层。这些方式叫做硬件加速方式
opacity(透明度)值的修改不会触发重排、重绘。因为实际上透明度的改变后,GPU在绘画时只是简单的降低之前已经画好的纹理的alpha值来达到效果,并不需要整体的重绘。不过这个前提是这个被修改本身必须是一个图层,如果图层下还有其他节点,GPU也会将他们透明化。
使用 transform
我们通过节点的transform可以修改节点的位置(translate)、旋转(rotate)、大小(scale)等。我们平常会使用lefttop属性来修改节点的位置,但正如上面所述,lefttop会触发重排,修改时的代价相当大。取而代之的更好方法是使用translate,这个不会触发重排!
/*
* 根据上面的结论
* 将 2d transform 换成 3d
* 就可以强制开启 GPU 加速
* 提高动画性能
*/
div {
transform: translate3d(10px, 10px, 0);
}
此部分内容摘抄自: 《知乎:css3的 translate 不会引起重排》
想要不晕还需要理解明白这些知识:浏览器内核 GUI 线程工作过程、重排和重绘触发条件、页面绘制过程
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
《你真的了解回流和重绘吗》
《浏览器的回流(重排)与重绘》
摘抄自:《前端性能优化(CSS动画篇)》

复合图层的作用?

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡。


思考回答

  • 简要描述浏览器回流和重绘的区别及对性能的影响,如何尽量避免对页面性能的影响?
  • 生成了render tree后再往下走会有两个阶段,reflow,repain。你有了解过他们各自都是干什么的吗?
  • 什么时候会触发重排,什么时候触发重绘?
  • 假如有一个漂浮的弹窗(即脱离了文档流),它有一些动画,比如突然展开、或者收起来,那么它会触发重排吗?

    参考:
    绝对布局虽然脱离了文档流,但不会创建新的复合图层,只是影响范围减小了,所以如果是对它宽、高、位置等的修改,会触发重排重绘。【具体原因见上方"(4)】
    但如果动画效果使用transform(如修改节点的位置(translate)、旋转(rotate)、大小(scale)等)、opacity 来完成的话,不会触发重排【具体原因见上方"(9) 开启新的复合图层"】

参考

原文链接
推荐文章里的'重绘(Repaint)和回流(Reflow)'部分
推荐阮一峰的《网页性能管理详解》
推荐《浏览器重绘(repaint)重排(reflow)与优化[浏览器机制]》

我的其他相关文章《浏览器渲染原理》
我的思维导图


添加新评论