回流与重绘| 青训营笔记

67 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天

什么是回流与重绘

当我们在浏览器运行一个页面时,他会首先解析HTML生成DOM树,解析CSS生成CSSDOM树,然后DOM树和CSSDOM树合并构建渲染树,然后再经过回流,回流主要是计算元素的形状、位置大小,然后再经过重绘,重绘就是转换为屏幕上的实际像素来达到页面展现的目的。下面我们通过字面意思来了解一下什么是回流和重绘

回流

  • 重点在于"流”,倾向于结构调整,对性能影响更大
  • 比如房子需要重新盖就是回流
  • 涉及元素的位置变化

重绘

  • 重点在于”绘”,倾向于样式调整,对性能影响较小
  • 房子需要重新装修就是重绘
  • 涉及元素的元素、背景色

回流一定触发重绘,而重绘不一定会回流

  • 比如我们盖房子就一定要重新装修
  • 如果我们换装修风格则不需要

引起回流的动作

  • 首次渲染页面

  • 添加、删除元素

  • 改变元素大小(内外边距、边框、宽高)

  • 改变元素位置

  • 改变元素内容

  • 改变字体大小

  • 调整浏览器窗口大小

  • 查询某些属性或调用某些方法,例如

image-20230209005812767.png

所以我们尽量避免使用以上方法

浏览器对回流和重绘的优化

浏览器维护着一个重绘和回流的队列,将需要重绘和回流的操作放置在这个队列当中,当队列中的重绘和回流的动作,达到一定数量会触发阈值,然后浏览器就会清空队列,批处理这些操作

如何避免回流

  • 避免频繁操作样式

    • 一次性改变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框架的源码中也有所体现。

参考

掘金小册:juejin.cn/book/684473…