在讨论回流和重绘之前,先了解一下:
- 浏览器使用流式布局模型
- 浏览器会把
HTML
解析成DOM
,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就产生了Render Tree
- 有了
Render Tree
,就可以每个节点对应的样式,然后计算他们在页面的大小和位置,最后把节点绘制在页面上。 - 由于浏览器使用流式布局,对
Render Tree
的计算通常只需要遍历一遍就可以完成了,但是table
及其内部的元素除外。 他们可能需要多次计算,通常需要 3 倍于同等元素的时间,这也是为什么要避免使用table
布局的原因之一。
我们可能已经从一些资料上了解到,当一个元素的可见性 visibility
发生改变的时候,重绘也随之发生,但是不影响布局。类似的属性还有 outline
、background-color
等。重绘的代价是高昂的,因为浏览器必须验证 DOM
树上其他节点元素的可见性。而回流更是影响性能的主要原因是它涉及部分页面或者整个页面的布局。
回流(重排)
当
Render Tree
中部分或者全部元素的尺寸、结构或某些属性发生改变时,浏览器重新渲染部分或者全部文档的过程称为回流。
ex:
<body>
<div class="error">
<h1>我的账户</h1>
<p><strong>错误:</strong>账户不存在...</p>
<h2>忘记账户?</h2>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>
上面示例中,对段落标签(<p>
)会留将会引发强烈的回流,因为它是一个子节点,这也导致祖先的回流(div.error
和 body
视浏览而定)。此外,<h5>
和 <ol>
也会有简单的回流,因为它们在 DOM
中的回流元素之后。
会导致回流的操作
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容的变化(文字数量或图片大小等等)
- 元素字体大小变化
- 脚本操作
DOM
(添加或删除可见DOM
元素等) - 操作
class
属性 - 激活
CSS
伪类(例如::hover
) - 查询某些属性或者调用某些方法
常用且会导致回流的属性和方法
clientWidth
,clientHeight
,clientTop
,clientLeft
offsetWidth
,offsetHeight
,offsetTop
,offsetLeft
scrollWidth
,scrollHeight
,scrollTop
,scrollLeft
getComputedStyle()
getBoundingClientRect()
scrollTo()
scrollIntoView()
,scrollIntoViewIfNeeded()
重绘
当页面中元素的样式改变并不影响它在文档流中的位置时(例如:
color
,background-color
,visibility
等)浏览器会将新样式赋给元素并重新绘制它,这个过程称为重绘
性能影响
回流的代价比重绘高。回流必将引起重绘,重绘不一定会引起回流。
有时候即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器对频繁的回流和重绘进行了优化。
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值,浏览器就会将队列清空,进行一次批处理,这样就可以把多次回流和重绘变成一次。
当你访问下面的属性或者方法的时候,浏览器会立即清空队列
width
,height
clientWidth
,clientHeight
,clientTop
,clientLeft
offsetWidth
,offsetHeight
,offsetTop
,offsetLeft
scrollWidth
,scrollHeight
,scrollTop
,scrollLeft
getComputedStyle()
getBoundingClientRect()
因为队列中可能会有影响这些属性或方法返回值的操作,即使你希望获取的信息可能与队列中的操作引发的改变无关,浏览器也会强行清空队列,确保拿到的值是最精确的。
如何避免回流或者将它们对性能的影响降到最低
CSS 的使用
- 如果想设定元素的样式,通过改变元素的
class
名,并尽可能在DOM
树的最末端改变class
。 - 避免设置多项内联样式。
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上。 - 权衡平滑和速度。
- 避免使用 table 布局。
- 避免使用 CSS 的 JavaScript 表达式(例如
calc()
)。
尽可能在 DOM
树的最末端改变 class
回流可以自上而下,或自下而上的回流的信息传递给周围的节点。回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的里面改变 class
,可以限制了回流的范围,使其影响尽可能少的节点。例如,你应该避免通过改变对包装元素类去影响子节点的显示。面向对象的 CSS 始终尝试获得它们影响的类对象(DOM 节点或节点),但在这种情况下,它已尽可能的减少了回流的影响,增加性能优势。
避免设置多项内联样式
我们都知道与 DOM 交互很慢。我们尝试在一种无形的 DOM 树片段组进行更改,然后整个改变应用到 DOM 上时仅导致了一个回流。同样,通过 style 属性设置样式导致回流。避免设置多级内联样式,因为每个都会造成回流,样式应该合并在一个外部类,这样当该元素的 class
属性可被操控时仅会产生一个 reflow。
动画效果应用到 position 属性为 absolute 或 fixed 的元素上
动画效果应用到 position 属性为 absolute 或 fixed 的元素上,它们不影响其他元素的布局,所它他们只会导致重新绘制,而不是一个完整回流。这样消耗会更低。
牺牲平滑度换取速度
Opera 还建议我们牺牲平滑度换取速度,其意思是指您可能想每次 1 像素移动一个动画,但是如果此动画及随后的回流使用了 100% 的 CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。动画元素每次移动 3 像素可能在非常快的机器上看起来平滑度低了,但它不会导致 CPU 在较慢的机器和移动设备中抖动。
避免使用 table 布局
避免使用 table 布局。可能您需要其它些避免使用 table 的理由,在布局完全建立之前,table 经常需要多个关口,因为 table 是个和罕见的可以影响在它们之前已经进入的 DOM 元素的显示的元素。想象一下,因为表格最后一个单元格的内容过宽而导致纵列大小完全改变。这就是为什么所有的浏览器都逐步地不支持 table 表格的渲染。然而有另外一个原因为什么表格布局时很糟糕的主意,根据 Mozilla,即使一些小的变化将导致表格 (table) 中的所有其他节点回流。
避免使用 CSS 的 JavaScript 表达式
这项规则较过时,但确实是个好的主意。主要的原因,这些表现是如此昂贵,是因为他们每次重新计算文档,或部分文档、回流。正如我们从所有的很多事情看到的:引发回流,它可以每秒产生成千上万次。
JavaScript 的使用
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性。 - 避免频繁操作 DOM,创建一个
documentFragment
,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 - 也可以先为元素设置
display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的 DOM 操作不会引发回流和重绘。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。