回流重绘
回流重绘
回流
回流又名重排,指改变几何属性的渲染。但感觉回流这个词较高大上,后续统称回流吧。
可理解为将整个网页填白,对内容重新渲染一次。只不过以人眼的感官速度去看浏览器回流是不会有任何变化的,若你拥有闪电侠的感官速度去看浏览器回流(实质是将时间调慢),就会发现每次回流都会将页面清空,再从左上角第一个像素点从左到右从上到下这样一点一点渲染,直至右下角最后一个像素点。每次回流都会呈现该过程,只是感受不到而已。
渲染树的节点发生改变,影响了该节点的几何属性,导致该节点位置发生变化,此时就会触发浏览器回流并重新生成渲染树。回流意味着节点的几何属性改变,需重新计算并生成渲染树,导致渲染树的全部或部分发生变化。
重绘
重绘指改变外观属性而不影响几何属性的渲染。相比回流,重绘在两者中会温和一些,后续谈到的 CSS 性能优化就会基于该特点展开
渲染树的节点发生改变,但不影响该节点的几何属性。由此可见,回流对浏览器性能的消耗是高于重绘的,而且回流一定会伴随重绘,重绘却不一定伴随回流
为何回流一定会伴随重绘呢?整个节点的位置都变了,肯定要重新渲染它的外观属性啊!
属性分类
以下对一些常用的几何属性和外观属性分类,其实同种分类的属性都有一些共同点,各位同学可自行感受:
- 几何属性:包括布局、尺寸等可用数学几何衡量的属性
- 布局:
display
、float
、position
、list
、table
、flex
、columns
、grid
- 尺寸:
margin
、padding
、border
、width
、height
- 外观属性:包括界面、文字等可用状态向量描述的属性
- 界面:
appearance
、outline
、background
、mask
、box-shadow
、box-reflect
、filter
、opacity
、clip
- 文字:
text
、font
、word
如何理解回流重绘
有无更好的方式可帮助理解回流重绘呢?答案是有的。
某一天星巴克发行一套很有纪念价值的杯子,男同胞们为了买到心仪的杯子给女友当惊喜礼物,通宵达旦搬张板凳去星巴克门口排队。此时形成的队伍是有序的,毕竟大家都是文明人,不可能随便插队吧,先到先拿,这个道理谁都懂!
可是总有一些人不按常理出牌,别人排队排得那么辛苦,他一到来就仗着自己有钱有势人多马多,插队到最前面。若他插队成功,那么后面的人都要往后挪一位。此时队伍就要重新往后挪,甚至引发多人斗殴。但混乱的情况总会被控制下来,此时就得重新排队,而原先的队伍顺序经过这次斗殴就可能不按照原先的队伍顺序排队了。几何属性变了,就要重新排队,这个就是回流或重排。重新排队啊 😂!
一位漂亮妹纸排队排得久肚子呱呱叫,就与另一位同伴交换,她去买早餐,而这位同伴代替她的位置。各位男同胞可能发现这位妹纸更漂亮了。没错,外观属性改变了,变漂亮了,但除了妹纸,其余人的位置和顺序都无发生变化,所以肯定不会发生上述重新排队的情况。外观属性变了,但几何属性没变,这个就是重绘。不用重新排队,还有漂亮妹纸看,大家都很乐意 🤔!
性能优化
回流重绘在操作节点样式时频繁出现,同时也存在很大程度上的性能问题。回流成本比重绘成本高得多,一个节点的回流很有可能导致子节点、兄弟节点或祖先节点的回流。在一些高性能电脑上也许无什么影响,但回流发生在手机上(明摆说某些安卓手机),就会减缓加载速度和增加电量消耗
在上一章中引出一个定向法则:回流必定引发重绘,重绘不一定引发回流,可利用该法则解决一些因为回流重绘而引发的性能问题。在优化性能前,需了解什么情况可能产生性能问题,以下罗列一些常见的情况。
- 改变窗口大小
- 修改盒模型
- 增删样式
- 重构布局
- 重设尺寸
- 改变字体
- 改动文字
很多同学可能不知,回流重绘其实与浏览器的事件循环有关,以下源自对HTML 文档的理解。
- 浏览器刷新频率为
60Hz
,即每16.6ms
更新一次 - 执行事件循环完成微任务
- 判断
document
是否需更新 - 判断
resize/scroll
事件是否存在,存在则触发事件 - 判断
Media Query
是否触发 - 更新动作并发送事件
- 判断
document.isFullScreen
是否为true
(全屏) - 执行
requestAnimationFrame
回调,每次重绘之前调用 - 执行
IntersectionObserver
回调,IntersectionObserver API - 回调的触发的时机问题 - 更新界面
上述就是浏览器每一帧中可能会做到的事情,若在一帧中有空闲时间,就会执行requestIdleCallback
回调
回到正题,通过定向法则回流必定引发重绘,重绘不一定引发回流可知道,尽量减少回流重绘,就是 CSS 性能优化中一个很好的指标。
如何减少和避免回流重绘
使用 visibility:hidden 替换 display:none
笔者从以下四方面对比display:none
和visibility:hidden
,display:none
简称DN
,visibility:hidden
简称VH
。
- 占位表现
- DN 不占据空间
- VH 占据空间
- 触发影响
- DN 触发回流重绘
- VH 触发重绘
- 过渡影响
- DN 影响过渡不影响动画
- VH 不影响过渡不影响动画
- 株连效果
- DN 后自身及其子节点全都不可见
- VH 后自身及其子节点全都不可见但可声明子节点
visibility:visible
单独显示
两者的占位表现、触发影响和株连效果就能说明VH
代替DN
的好处,从两者区别中就能找出恰当的答案了
使用 transform 代替 top
top
是几何属性,操作top
会改变节点位置从而引发回流,使用transform:translate3d(x,0,0)
代替top
,只会引发图层重绘,还会间接启动 GPU 加速,该情况在第 12 章变换与动画中会详细讲述。
避免使用 Table 布局
牵一发而动全身用在 Table
布局身上就很适合了,可能很小的一个改动就会造成整个<table>
回流,有兴趣的同学可用Chrome Devtools
的Performance
调试看看,在此就不演示了
通常可用<ul>
、<li>
和<span>
等标签取代<table>
系列标签生成表格。
避免规则层级过多
浏览器的CSS解析器
解析css文件
时,对 CSS 规则是从右到左匹配查找,样式层级过多会影响回流重绘效率,建议保持 CSS 规则在3层
左右。
避免节点属性值放在循环里当成循环变量
for (let i = 0; i < 10000; i++) {
const top = document.getElementById('css').style.top
console.log(top)
}
呵呵,每次循环操作 DOM 都会发生回流,应该在循环外使用变量保存一些不会变化的 DOM 映射值
const top = document.getElementById('css').style.top
for (let i = 0; i < 10000; i++) {
console.log(top)
}
动态改变类而不改变样式
不要尝试每次操作 DOM 去改变节点样式,这样会频繁触发回流
更好的方式是使用新的类名预定义节点样式,在执行逻辑操作时收集并确认最终更换的类名集合,在适合时机一次性动态替换原来的类名集合。有点像vue
的依赖收集机制
,不知这样描述会不会更容易理解
各位同学可研究下这个强大的classList,它能满足笔者所说的需求
将频繁回流重绘的节点设置为图层
上一章的渲染过程
最后一步,提到将回流重绘生成的图层逐张合并并显示在屏幕上。可将其理解成Photoshop
的图层,若不对图层添加关联,图层间是不会互相影响的。同理,在浏览器中设置频繁回流或重绘的节点为一张新图层,那么新图层就能够阻止节点的渲染行为影响别的节点,这张图层里如何变化都无法影响到其他图层
设置新图层有两种方式,将节点设置为<video>
或<iframe>
,为节点添加will-change
。will-change
是一个很叼的属性,在第 12 章变换与动画中会详细讲述
使用 requestAnimationFrame 作为动画帧
动画速度越快,回流次数越多,上述有提到浏览器刷新频率为60Hz
,即每16.6ms
更新一次,而requestAnimationFrame()
正是以16.6ms
的速度更新一次。所以可用requestAnimationFrame()
代替setInterval()
。