回流与重绘
重绘(repaint),回流也称为重排(reflow)
前言
这是一道很经典的前端八股面试题了,不过实际开发也是有作用的
在做 UI 效果的时候一般考虑的就是俩个方面:
- 美观层面:布局样式好不好看
- 性能层面:浏览器渲染性能,会不会触发回流和重绘
这里就围绕其中的回流与重绘展开谈谈
- 什么是浏览器的回流重绘?
- 如何在实际项目中考虑他们产生的性能问题?
- 如何权衡他们
重绘
某些元素的外观改变所出发的浏览器行为,不过没有改变布局,把元素外观重新绘制出来的过程就叫重绘
常见会引起重绘的属性:
color |
border-style |
visibility |
background |
text-decoration |
background-image |
background-position |
background-repeat |
outline-color |
outline |
outline-style |
border-radius |
Outline-width |
box-shadow |
background-size |
|
重排
某些元素的位置或尺寸发生了变化,浏览器需要重新生成布局,重新排列元素,把元素放到争取的位置的过程叫重排
回流就好比向河里(文档流)扔了一块石头(dom 变化),激起涟漪,然后引起周边水流受到波及,所以也叫做回流
通过概念对比也可以简单的发现,重排一定导致重绘,重排的成本大很多
任何改变元素的尺寸/位置的操作都会触发重排,比如:
- 页面初始渲染,这是开销最大的一次重排;
- 添加/删除可见的 DOM 元素;
- 改变元素位置;
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
- 改变元素内容,比如文字数量,图片大小等;
- 改变元素字体大小;
- 改变浏览器窗口尺寸,比如 resize 事件发生时;
- 激活 CSS 伪类(例如::hover);
- 设置 style 属性的值,因为通过设置 style 属性改变结点样式的话,每一次设置都会触发一次 reflow;
- 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight 等,除此之外,当我们调用 getComputedStyl 方法,或者 IE 里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”;
影响范围
全局范围,比如下面这个:
<body> <div class="hello"> <h4>hello</h4> <p><strong>Name:</strong>juns</p> <h5>male</h5> <ol> <li>coding</li> <li>loving</li> </ol> </div> </body>
|
如果 p 节点发生了 reflow,那么其他的都会受到影响,hello、body、h5、ol 都可能受到影响
局部范围:
可以理解吧一个 dom 的宽高固定,然后在这个 dom 内部发生 reflow,就只会重新渲染他内部的元素
如何优化
核心目标:减少重排次数,减小重排范围
样式集中改变
将原本一行行添加样式的代码,合并到一个类名中,直接给元素添加类名
<div id="demo">我是demo</div>
<script> const demo = document.getElementById('demo') demo.style.color = 'red' demo.style.background = '#ccc' demo.style.padding = '15px 20px' </script>
|
上面这个 js 代码会引起 3 次重绘,1 次重排,可以通过添加类名的方式,使他只有 1 次重排
<div id="demo">我是demo</div>
<script> const demo = document.getElementById('demo') demo.className = 'demo' </script> <style> .demo { color: red; background: #ccc; padding: 15px 20px; } </style>
|
将 DOM 离线
简单说就是先隐藏,调样式,再切换为显示
<div id="demo">我是demo</div>
<script> const demo = document.getElementById('demo') demo.style.color = 'red' demo.style.background = '#ccc' demo.style.padding = '15px 20px'
demo.style.marginLeft = '15px' demo.style.marginTop = '15px'
demo.style.border = '2px solid red' </script>
|
修改为:
<div id="demo">我是demo</div>
<script> const demo = document.getElementById('demo') demo.style.display = 'none' demo.style.color = 'red' demo.style.background = '#ccc' demo.style.padding = '15px 20px'
demo.style.marginLeft = '15px' demo.style.marginTop = '15px'
demo.style.border = '2px solid #ccc' demo.style.display = 'block' </script>
|
这样就只有 2 次重排了,在切换显示隐藏的时候
脱离文档流
可以使用 absolute 或 fixed 来脱离文档流,减少重排的范围
<div id="demo">我是demo</div> <div class="other">我是其他</div>
<script> const demo = document.getElementById('demo') demo.style.padding = '15px 20px' demo.style.height = '60px' </script>
|
可以在特定场景下,把一些 dom 脱离文档流
<div id="demo">我是demo</div> <div class="other">我是其他</div>
<script> const renderEle = document.getElementById('demo') demo.style.position = 'fixed' demo.style.padding = '15px 20px' demo.style.height = '60px' </script>
|
在内存中操作 DOM
这个就像是虚拟 DOM 的理念了,在 js 中修改 DOM,然后在一次性渲染成真实的 DOM
<div id="demo"> <ul id="father"> <li>我是0号,我后面还有1号、2号、3号</li> </ul> </div>
<script> const father = document.getElementById('father') let arr = [] setTimeout(() => { arr = ['我是1号', '我是2号', '我是3号'] arr.forEach(element => { const childNode = document.createElement('li') childNode.innerText = element father.appendChild(childNode) }) }, 1000) </script>
|
这个会导致多次重排,可以改为一次构建整个 ul
<div id="demo"> <ul id="father"> <li>我是0号,我后面还有1号、2号、3号</li> </ul> </div>
<script> const demo = document.getElementById('demo') const childUlNode = document.createElement('ul') let arr = [] setTimeout(() => { arr = ['我是1号', '我是2号', '我是3号'] arr.forEach(element => { const childLiNode = document.createElement('li') childLiNode.innerText = element childUlNode.appendChild(childLiNode) }) }, 1000) demo.appendChild(childUlNode) </script>
|
读写分离
浏览器会对重排做出优化,比如这样的操作
div.style.left = '10px' div.style.top = '10px' div.style.width = '20px' div.style.height = '20px'
|
根据我们的知识来看,应该是 4 次重排+重绘,不过实际上只触发了最后一次重排,这得益于浏览器的渲染队列机制。当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
不过当我们读取某些信息的时候,浏览器会立即执行渲染队列的任务
div.style.left = '10px' console.log(div.offsetLeft) div.style.top = '10px' console.log(div.offsetTop) div.style.width = '20px' console.log(div.offsetWidth) div.style.height = '20px' console.log(div.offsetHeight)
|
这样就会在我们读取之前,浏览器就立即执行重排+重绘,所以说我们可以把读写分离,这样也可以优化
div.style.left = '10px' div.style.top = '10px' div.style.width = '20px' div.style.height = '20px' console.log(div.offsetLeft) console.log(div.offsetTop) console.log(div.offsetWidth) console.log(div.offsetHeight)
|
我们也可以缓存布局信息:
div.style.left = div.offsetLeft + 1 + 'px' div.style.top = div.offsetTop + 1 + 'px'
var curLeft = div.offsetLeft var curTop = div.offsetTop div.style.left = curLeft + 1 + 'px' div.style.top = curTop + 1 + 'px'
|