回流(reflow)和重绘(repaint)
简单回顾浏览器的渲染机制
浏览器从下载文档到显示页面的过程是个复杂的过程,其中就包含了回流和重绘。
通常在文档初次加载时,浏览器引擎会使用 HTML 解析器解析 HTML 文档,构建 DOM 树
使用 CSS 解析器解析 CSS,构建 CSSOM 树(或叫 Style Rules)
DOM 树和 CSSOM 树结合,生成一棵渲染树(Render Tree)。渲染树的每个节点都有大小和边距等属性,类似于盒子模型(由于隐藏元素不需要显示,渲染树中并不包含 DOM 树中隐藏的元素)。
当渲染树构建完成后,我们就知道了所有节点的样式,浏览器即可开始计算它们要占据的空间大小及其在屏幕的位置,生成布局(Layout Flow)
最后再将布局绘制(paint)在屏幕上。绘制是填充像素的过程,它涉及绘出文本、颜色、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个层上完成,它们需要按正确的顺序将每个层绘制到屏幕上。
浏览器采用的是流式布局模型(Flow Based Layout),对于 Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,它们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。不过网页的布局模式也意味着一个元素可能影响其他元素,例如:元素的宽度一般会影响其子元素的宽度以及树中各处节点,因此对于浏览器来说布局过程是经常发生的(reflow)。
其中上面的第四步和第五步是最耗时的部分,这两步结合起来,就是我们通常所说的渲染。网页生成显示在屏幕上,至少会渲染一次(Layout Flow + Paint),在用户访问的过程中,还会不断重新渲染。 重新渲染需要重复之前的第四步(重新生成布局)+ 第五步(重新绘制)或者只有第五步(重新绘制)。

重绘(repaint)
概念
当一个元素外观发生变化,但没有改变布局时,浏览器根据元素的新属性重新绘制,使元素呈现新的外观的过程,叫做重绘。
常见的引起重绘的属性
color
visibility
box-shadow
text-decoration
border-style
border-radius
background
background-color
background-image
background-position
background-repeat
background-size
outline
outline-color
outline-style
outline-width
回流(reflow)
概念
当 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性(可以理解为渲染树需要重新计算),将其安放在界面中的正确位置,这个过程叫做回流。回流也叫做重排。
有一个比较形象比喻:回流就好比想河流里(文档流)扔了一块石头(DOM 变化),激起涟漪,然后引起周边的水流受到波及,所以叫做回流。
常见的引起回流的情况
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。
DOM 元素的几何属性发生变化,如元素尺寸改变——margin、padding、border、width 和 height
当 DOM 元素的几何属性变化时,渲染树中的相关节点就会失效,浏览器会根据 DOM 元素的变化重新构建渲染树中失效的节点。之后,会根据新的渲染树重新绘制出这部分页面。而且,当前元素的回流也会带来相关元素的回流。例如:容器节点的渲染树改变时,会触发子节点的重新计算,也会触发其后续兄弟节点的回流,祖先节点需要重新计算子节点的尺寸也会产生回流。最后,每个元素都将发生重绘。
DOM 树的结构发生变化,例如添加或删除可见的 DOM 元素
当 DOM 树的结构变化时,例如节点的增减和移动等,也会触发回流。浏览器引擎布局的过程。类似于树的前序遍历,是一个从上到下,从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。所以,如果在 body 最前面插入一个元素,会导致整个文档的重新渲染,而在其后插入一个元素,则不会影响到前面的元素。
内容变化,比如用户在 input 框中输入文字
浏览器窗口尺寸改变——resize 时间发生时
获取某些属性,例如计算 offsetWidth 和 offsetHeight 属性
设置 style 属性的值
能引起回流的属性和方法
width
height
padding
border
margin
poisiton
display
overflow
clientWidth
clientHeight
clientTop
clientLeft
offsetWidth
offsetHeight
offsetTop
offsetLeft
scrollWidth
scrollHeight
scrollTop
scrollLeft
scrollIntoView()
scrollTo()
getComputedStyle()
getBoundingClientRect()
scrollIntoViewIfNeeded()
回流的影响
影响范围
由于浏览器渲染界面是基于流式布局模型的,所以触发回流会对周围 DOM 重新排列,影响的范围有两种:
全局范围:从根节点 html 开始对整个渲染树进行重新布局。一般不加限制的情况下通常会在全局范围引发回流。
局部范围:对渲染树中某部分或某一个渲染对象进行重新布局。
例如把一个 DOM 的宽高等几何信息写死,然后在 DOM 内部触发回流,就只会渲染该 DOM 内部的元素,不会影响到外界。
此外 CSS 新特性 contain 也会控制页面的重绘与回流。
contain
属性允许我们指定特定的 DOM 元素和它的子元素,让他们能够独立于整个 DOM 树结构之外。目的是能够让浏览器有能力只对部分元素进行重绘、回流,而不必每次都针对整个页面。详情可查看此文章把 position 属性设置成 absolute 或 fixed 等操作,让元素脱离文档流
影响性能
回流需要更新渲染树,性能开销非常大,会破坏用户体验,并且让 UI 展示非常迟缓,我们需要尽可能的减少触发回流的次数。
回流的性能花销跟渲染树有多少个节点需要重新构建有关系,所以我们应该尽量以局部布局的形式组织 html 结构,尽可能减小回流的影响范围,而不是一味地堆砌标签,随便一个元素触发回流都会导致全局范围的回流。
重绘和回流的关系
回流比重绘影响要大,单单改变元素的外观,肯定不会引起网页重新生成布局,但当元素的尺寸发生改变,浏览器完成回流之后,将会重新绘制受到此次回流影响的部分。重绘不会带来重新布局,因此重绘不一定会引发回流,但是回流必然会发生重绘。
重绘和回流对浏览器性能的影响也是有区别的:重绘的代价很高,因为浏览器必须验证 DOM 树上其他节点元素的可见性,但是回流的代价更高,回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致其所有子元素以及 DOM 中紧随其后的兄弟节点、祖先节点等元素的回流。
浏览器优化
现代浏览器大多都对重绘和回流做了一些优化,一般都是通过渲染队列机制来批量更新布局。当我们修改了元素的外观或几何属性,会导致浏览器触发回流或重绘时,浏览器会把修改操作放到渲染队列中,等到队列中的操作到了**一定数量或者到了一定的时间间隔(至少一个浏览器刷新,即 16.6ms)**才会清空队列。
但是当我们主动获取布局信息的时候,渲染队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
/** 以下四次修改只会触发一次渲染(回流+重绘) */
div.style.left = '10px'
div.style.top = '10px'
div.style.width = '20px'
div.style.height = '20px'
/** 以下代码会发生4次渲染(回流+重绘),因为使用console.log请求了这几个样式信息,即使该值与你操作中修改的值没有关联,也会立即执行渲染队列中的任务,这是浏览器为了给我们最精确的值 */
div.style.left = '10px'
console.log(div.offsetLeft)
div.style.top = '10px'
console.log(div.offsetTop)
div.style.width = '20px'
console.log(div.offsetWidth)
div.style.height = '20px'
console.log(div.offsetHeight)
强制清空渲染队列的 style 样式请求:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
width、height
getComputedStyle()
getBoundingClientRect()
IE 的 currentStyle
我们在开发中,应该谨慎的使用这些 style 请求,注意上下文关系,避免一行代码触发一次回流,这对性能是个巨大的消耗。
优化重绘和回流的建议
1、分离读写操作
针对上面发生四次渲染(回流+重绘)的代码,我们可以通过分离读写的操作,实现只触发一次渲染(回流+重绘)
div.style.left = '10px'
div.style.top = '10px'
div.style.width = '20px'
div.style.height = '20px'
console.log(div.offsetLeft)
console.log(div.offsetTop)
console.log(div.offsetWidth)
console.log(div.offsetHeight)
前面的样式设置,浏览器会优化到渲染队列中,然后在第一次 console.log 的时候,浏览器会把渲染队列清空,剩下的 console.log,因为渲染队列本来就是空的,所以没有触发渲染(回流+重绘)。
2、将多次改变样式属性的操作合并成一次操作
var changeDiv = document.getElementById('changeDiv')
changeDiv.style.color = '#093'
changeDiv.style.background = '#eee'
changeDiv.style.height = '200px'
虽然现在大部分浏览器有渲染队列机制优化,不排除有些浏览器以及老版本的浏览器效率仍然地下,所以建议通过改变 class 或者 csstext 属性合并修改样式。
div.changeDiv {
background: #eee;
color: #093;
height: 200px;
}
document.getElementById('changeDiv').className = 'changeDiv'
3、将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点
将需要多次回流的元素的 position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化所影响的范围就会比较小。
启用 GPU 加速 GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。GPU 加速通常包括以下几个部分:Canvas2D、布局合成、will-change、CSS3 3D 转换(transform)、WebGL 和视频(video)。
4、缓存布局信息
在需要经常获取那些引起浏览器重排的属性值时,要缓存到变量中。其实还是在分离读写操作,而且进一步缓存起来,能避免多次读取属性值。
// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px'
div.style.top = div.offsetTop + 1 + 'px'
// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft
var curTop = div.offsetTop
div.style.left = curLeft + 1 + 'px'
div.style.top = curTop + 1 + 'px'
5、离线操作 DOM
由于 display 属性为 none 的元素不再渲染树中,对隐藏的元素操作不会一你发其他元素的回流,如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示,这样只在隐藏和显示时触发两次回流。
通过使用 DocumentFragment 创建一个 DOM 碎片,在它上面批量操作 DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。
复制节点,在副本上操作,然后替换原节点。
总结
重绘和回流会不断触发,这是不可避免的。但是我们再开发时,应尽量按照以上的建议来组织代码。
参考文档
Last updated
Was this helpful?