0%

回流重绘之性能优化

实习的时候遇到一个写一个tag-input的需求,也就是在input输入后里面每次回车都生成一个标签,在每次改变input的padding的时候都涉及到回流重绘的问题,为了优化性能,减少回流重绘的次数,又复习了一遍回流重绘。

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

浏览器的优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

因此浏览器不得不清空队列,触发回流重绘来返回正确的值,造成强制同步布局

减少回流和重绘

最小化重绘和重排

由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。

使用cssText:

比如我的tag-input组件,优化前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
methods:{
delTag(index){
this.tags.splice(index,1)
this.$nextTick(()=>{
let el = document.querySelector('.yh-tag:last-child')
let top = el.offsetTop - el.clientHeight
let left = el.offsetLeft + el.clientWidth
document.querySelector('.yh-tag-input .el-textarea__inner').style.paddingLeft = left + 10 +'px'
document.querySelector('.yh-tag-input .el-textarea__inner').style.paddingTop = top + 10 +'px'
document.querySelector('.yh-tag-input .el-textarea__inner').style.height = el.offsetTop + 40 +'px'
})
},
addTag(){
if(!this.inputData) return ;
this.tags.push(this.inputData)
this.$nextTick(()=>{
let el = document.querySelector('.yh-tag:last-child')
let top = el.offsetTop - el.clientHeight
let left = el.offsetLeft + el.clientWidth
document.querySelector('.yh-tag-input .el-textarea__inner').style.paddingLeft = left + 10 +'px'
document.querySelector('.yh-tag-input .el-textarea__inner').style.paddingTop = top + 10 +'px'
document.querySelector('.yh-tag-input .el-textarea__inner').style.height = el.offsetTop + 40 +'px'
})
this.inputData = ''
}
}
}

优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
methods:{
delTag(index){
this.tags.splice(index,1)
this.$nextTick(()=>{
// tag-box布局为absolute 重排开销较小 不用考虑对其他元素的影响
let el = document.querySelector('.yh-tag:last-child')
if(el) {
// 缓存布局信息,读写分离,一次回流+重绘
let top = el.offsetTop;
let left = el.offsetLeft + el.clientWidth
// 放入浏览器队列中执行
document.querySelector('.yh-tag-input .el-input__inner').style.cssText =
`padding-left:${left + 10}px;padding-top:${top - 5}px;height:${top+30}px`;
} else {
document.querySelector('.yh-tag-input .el-input__inner').style.cssText =
`padding-left:5px;padding-top:5px;`
}
})
},
addTag(){
if(!this.inputData) return ;
this.tags.push(this.inputData)
this.$nextTick(()=>{
let el = document.querySelector('.yh-tag:last-child')
if(el) {
let top = el.offsetTop
let left = el.offsetLeft + el.clientWidth
document.querySelector('.yh-tag-input .el-input__inner').style.cssText=
`padding-left:${left + 10}px;padding-top:${top - 5}px;height:${top+30}px`;
}
})
this.inputData = ''
},
focus() {
this.$refs.input.focus();
}
}

或者改为使用class

更进一步优化

input-tag组件就是输入后失去焦点或者按下回车形成标签,并且光标始终跟随在最后,要可以自动换行,起初的做法是input后形成的标签用el-tag显示,并且el-tag是子组件,定位为绝对位置,父盒子相对位置,卡在了获取每次点击无法去自动获取光标,起初通过document.querySelector拿到最后一个标签元素,设置input的padding-left为最后一个el-tag的el.offsetLeft+el.clientWidth,padding-top为el.offsetTop,但是这样每次去读取的时候会最少造成一次回流,感觉交互体验一般,因此,解决办法就是将input放到和el-tag同级上面,并且设置外面的盒子为display:flex,flex-wrap:warp,这样input会跟着el-tag走,光标自动锁定。

批量修改DOM,读写dom分离

1
2
3
4
5
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}

上面这段代码每次循环都读取box的offsetWidth属性值,再写style.width,每次循环都会强制浏览器刷新对列,可以将width缓存

1
2
3
4
5
6
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}

css3硬件加速(GPU加速)

1. 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。

2. 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

因为使用css动画在合成线程上执行,如果采用GPU进行栅格化,合成效率会更高