实习的时候遇到一个写一个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(()=>{ 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进行栅格化,合成效率会更高