回流与重绘

回流与重绘

重绘(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>
// 第一次操作修改 color、background、padding
const demo = document.getElementById('demo')
demo.style.color = 'red' // 重绘
demo.style.background = '#ccc' // 重绘
demo.style.padding = '15px 20px' // 重排, 引起重绘

// 第二次操作修改 marginLeft、marginTop
demo.style.marginLeft = '15px' // 重排, 引起重绘
demo.style.marginTop = '15px' // 重排, 引起重绘

// 第三次操作修改 border
demo.style.border = '2px solid red' // 重排, 引起重绘
</script>

修改为:

<div id="demo">我是demo</div>

<script>
const demo = document.getElementById('demo')
// 第一次操作修改 color、background、padding
demo.style.display = 'none' // 重排, 重绘
// DOM不存在渲染树上不会引起重排、重绘
demo.style.color = 'red'
demo.style.background = '#ccc'
demo.style.padding = '15px 20px'

// 第二次操作修改 marginLeft、marginTop
demo.style.marginLeft = '15px'
demo.style.marginTop = '15px'

// 第三次操作修改 border
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)

我们也可以缓存布局信息:

// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px'
div.style.top = div.offsetTop + 1 + 'px'

// good 缓存布局信息 相当于读写分离
var curLeft = div.offsetLeft
var curTop = div.offsetTop
div.style.left = curLeft + 1 + 'px'
div.style.top = curTop + 1 + 'px'