回流、重绘与合成
一、回流 Reflow
1、什么是回流
回流(Reflow)也叫重排,指的是元素的尺寸、结构或位置发生了变化,浏览器会触发重新计算样式和渲染树的过程。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。
2、常见的回流触发条件
- 页面首次渲染
- 元素尺寸或位置发生改变
- 元素内容结构变化(文本长度或图片大小等)
- 添加或者删除可见的
DOM
元素 - 浏览器窗口大小发生改变
- 激活
CSS
伪类(如::hover
)
触发回流常见的属性和方法:
// 属性
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
// 方法
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
3、回流示例
这里实现点击 demo 按钮时,给盒子 #app 添加一个内联样式 app.style.marginLeft = '100px'
。
此时打开开发者工具 performance 查看:
可以看到,回流重绘都有发生,也就是完整的 JS / CSS > 样式 > 布局(Layout) > 绘制(Paint) > 合成
二、重绘 Repaint
1、什么是重绘
重绘(Repaint)是指元素的颜色、悬浮效果等不脱离文档流的样式发生改变时,浏览器将新样式赋给元素并重新绘制的过程。
2、与回流的区别
回流比重绘的性能消耗更高,而且回流一定引起重绘,重绘不一定会引起回流。
3、重绘示例
这里实现点击 demo 按钮时,给盒子 #app 添加一个内联样式 app.style.backgroundColor = '#66ccff'
。
此时打开开发者工具 performance 查看:
可以看到,只发生重绘,而不发生回流。也就是一次 JS / CSS > 样式 > 绘制(Paint) > 合成
三、合成 Composite
1、什么是合成
合成(Composite)指的是修改一个既不用布局也不用绘制的属性,浏览器渲染引擎将跳过布局和绘制,直接执行后续的合成操作,合成没有涉及到主线程以及相关的样式计算,因此合成的动画最流畅。
比如我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
2、合成示例
需求:方块向右移动 200px
- 方法一:使用 margin 来实现。
<style>
#app {
width: 200px;
height: 200px;
background-color: #39c5bb;
transition: 2s ease;
margin-left: 0;
}
</style>
<body>
<button id="click">开始</button>
<div id="app"></div>
<script>
const app = document.querySelector('#app')
const button = document.querySelector('#click')
button.addEventListener('click', () => {
setTimeout(() => {
app.style.marginLeft = '100px'
}, 2000)
})
</script>
</body>
实现图示如下:
可以看到,每一帧(这里是16.2ms)都会发生一次回流重绘,动画出现了明显的掉帧现象。
- 方法二:使用 transform 来实现。
transform 产出的动画只会发生 composite 合成这一步骤。
<style>
#app {
width: 200px;
height: 200px;
background-color: #39c5bb;
transition: 2s ease;
transform: translateX(0);
}
</style>
<body>
<button id="click">开始</button>
<div id="app"></div>
<script>
const app = document.querySelector('#app')
const button = document.querySelector('#click')
button.addEventListener('click', () => {
setTimeout(() => {
app.style.transform = 'translateX(100px)'
}, 2000)
})
</script>
</body>
实现图示如下:
可以看到,帧数很稳定,动画过程中只会发生 composite 合成这一步骤。
目前大多数设备的屏幕刷新率 60fps。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。此现象通常称为卡顿,会对用户体验产生负面影响。
四、触发回流、重绘与合成条件汇总
「点击查看 CSS Triggers 的触发回流、重绘与合成的样式汇总」
五、如何减少回流和重绘
1、CSS 方面的优化
- 使用
transform
实现元素位移; - 为避免影响其他元素的布局,将动画效果应用到
position
属性为absolute
或fixed
的元素上; - 避免 CSS 表达式的使用(例如
calc()
),因为可能会触发回流。 - 使用 CSS3 硬件加速(GPU 加速),可以让
transform
、opacity
、filters
这些动画不会引起回流重绘。但对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
2、JS 方面的优化
- 避免频繁操作样式;
- 避免频繁操作 DOM,如果需要创建一个列表,可以使用字符模板串的反引号包裹,最后再
innerHTML
进去,也可以先为元素设置display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行 DOM 操作不会触发回流和重绘。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 使用 requestAnimationFrame 替代
setTimeout
/setInterval
来执行动画类的视觉变化,避免丢帧。 - 监听窗口变化,浏览器滚动等性能开销特别大的功能时,添加防抖。