这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天
什么是回流与重绘
当我们在浏览器运行一个页面时,他会首先解析HTML生成DOM树,解析CSS生成CSSDOM树,然后DOM树和CSSDOM树合并构建渲染树,然后再经过回流,回流主要是计算元素的形状、位置大小,然后再经过重绘,重绘就是转换为屏幕上的实际像素来达到页面展现的目的。下面我们通过字面意思来了解一下什么是回流和重绘
回流
- 重点在于"流”,倾向于结构调整,对性能影响更大
- 比如房子需要重新盖就是回流
- 涉及元素的位置变化
重绘
- 重点在于”绘”,倾向于样式调整,对性能影响较小
- 房子需要重新装修就是重绘
- 涉及元素的元素、背景色
回流一定触发重绘,而重绘不一定会回流
- 比如我们盖房子就一定要重新装修
- 如果我们换装修风格则不需要
引起回流的动作
-
首次渲染页面
-
添加、删除元素
-
改变元素大小(内外边距、边框、宽高)
-
改变元素位置
-
改变元素内容
-
改变字体大小
-
调整浏览器窗口大小
-
查询某些属性或调用某些方法,例如
所以我们尽量避免使用以上方法
浏览器对回流和重绘的优化
浏览器维护着一个重绘和回流的队列,将需要重绘和回流的操作放置在这个队列当中,当队列中的重绘和回流的动作,达到一定数量会触发阈值,然后浏览器就会清空队列,批处理这些操作
如何避免回流
-
避免频繁操作样式
- 一次性改变style属性
- 增减class属性
-
避免频繁操作dom
- 比如让dom元素脱离文档流,然后修改,放回
- 可以通过操作文档碎片->然后dom操作->添加回文档
- 或者先让dom隐藏,然后修改,最后再显示
-
避免使用以上方法
-
使用绝对定位让执行动画的元素来脱离文档流
- 然后他会触发css硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘
对症下药,如何对DOM进行优化
假设有一个页面,内容如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body>
<div id="container"></div>
</body>
</html>
假设我现在有一个需求,往container元素里插入100个元素
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>test</span>'
}
这段代码有两个明显的可优化点。我们每一次循环都调用dom接口获取container元素,相当的浪费,我们可以缓存下来
let container = document.getElementById('container')
for(let count=0;count<10000;count++){
container.innerHTML += '<span>test</span>'
}
第二,我们的10000次循环中,每次都修改了dom,这必然引起渲染树的回流/重绘,所以我们可以先循环,再渲染
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){
// 先对内容进行操作
content += '<span>test</span>'
}
// 内容处理好了,最后再触发DOM的更改
container.innerHTML = content
利用DOM Fragment优化
DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
可以看出,DOM Fragment 对象允许我们像操作真实 DOM 一样去调用各种各样的 DOM API,我们先插入到文档碎片中,然后他会自身缓存,当循环结束后我们才操作真实的dom。这个api的使用在vue框架的源码中也有所体现。