浏览器的优化策略下重绘和回流会发生几次?

重绘和回流(重排)

本文是对重绘和回流知识的整理,已经熟悉的可直接跳到标题内容

为了方便个人回顾复习,不用在冗长的代码示例中寻找关键信息,本文所有的代码示例都被折叠起来。如果是第一次阅读,建议在阅读过程中展开折叠部分,有助于加深理解和记忆。

一、网页渲染流程

  1. 解析HTML代码,生成DOM
  2. 解析CSS代码,生成CSSOM
  3. 将DOM树和CSSOM树结合,去除不可见的元素(如display: none的元素),构建渲染树Render Tree
  4. 计算布局(回流 | 重排 ),根据Render Tree进行布局计算,得到每一个节点的几何信息
  5. 绘制页面(重绘),GPU根据布局信息绘制

二、概念

重绘(Repaint) :当元素的外观发生改变 ,但没有改变布局(如改变颜色)时,浏览器将重新绘制该元素,这个过程称为重绘。

回流(Reflow,或重排) :当元素的尺寸或者位置发生改变时(如修改元素的宽高),浏览器需要重新计算元素的几何属性,并且可能导致其他元素的几何属性也发生改变。这个过程称为回流。回流涉及到页面布局和元素位置的更新,通常比重绘更消耗性能。

重绘不一定导致重排,但重排一定会导致重绘。

三、什么情况发生重绘和重排

发生重绘:颜色、背景色、边框样式等

colorborder-styleborder-radiustext-decorationbox-shadowoutlinebackground ...

发生重排:各种尺寸、位置等

  • 页面初始渲染,这是开销最大的一次重排;
  • 添加/删除可见的DOM元素;
  • 改变元素位置;
  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
  • 改变元素内容,比如文字数量,图片大小等;
  • 改变元素字体大小;
  • 改变浏览器窗口尺寸,比如resize事件发生时;
  • 激活CSS伪类(例如::hover);
  • 设置 style 改变属性的样式;
  • 查询某些属性或调用某些计算方法:offsetWidth

四、怎么减少重绘和重排

浏览器的优化策略

现在的浏览器是有一个优化策略的,它执行js的时候会维护一个渲染队列 ,改变一个容器的样式,导致需要发生回流的时候,这个操作不会立即执行 ,而是会进入渲染队列,如果还有连续的样式修改,会继续进入队列,直到没有样式修改,浏览器会批量化地执行渲染队列中的回流过程 ,这只发生一次回流,而不是每次修改都触发一次。
例子1:仅持续写,有几次回流?

js 复制代码
<div id="app">Hello</div>

<script>
    let app = document.getElementById('app')

    app.style.position = 'relative';
    app.style.width = '100px'
    app.style.height = '200px'
    app.style.left = '10px'
    app.style.top = '10px'
</script>

不算浏览器初次加载,是一次回流
例子2:写一行读一行,有几次回流?

js 复制代码
app.style.width = '100px'
console.log(app.offsetWidth) 
app.style.height = '200px'
console.log(app.offsetHeight)
app.style.left = '10px'
console.log(app.offsetLeft)
app.style.top = '10px'
console.log(app.offsetTop)

四次回流 ,同步代码从上往下执行,执行打印语句的时候无法跳过。写时进入渲染队列,当要读取几何信息的时候,是会强制执行渲染队列,也就引起了重排,以确保返回的是最新的布局信息。 持续往复... 因此是四次回流。
例子3:全部写完再读,有几次回流?

js 复制代码
app.style.width = '100px'
app.style.height = '200px'
app.style.left = '10px'
app.style.top = '10px'

console.log(app.offsetLeft)
console.log(app.offsetHeight)
console.log(app.offsetWidth) 
console.log(app.offsetTop)

一次回流 。四次写操作依次进入渲染队列,执行第一个打印语句的时候,强制执行渲染队列,引起一次回流。后续渲染队列就空了,虽然继续读,但空了的队列无法引起回流 ,读取值是不引起回流的,读取值引起的有内容的队列执行才会引起回流
例子4:同一行有写有读,有几次回流?(字节面试题)

js 复制代码
let el = document.getElementById('app')
el.style.width = (el.offsetWidth + 1) + 'px' 
el.style.width = 1 + 'px'

一次回流 。第一行获取dom结构不造成回流。看第二行,从左往右执行,左边会进入渲染队列,但是右边会获取几何信息,offsetWidth是强制触发渲染队列的执行,而非强制回流。这里渲染队列里面没有东西去让你执行,所以执行第二行,第三行会连续入渲染队列,最后只发生一次回流。

总结 :只要是读取几何信息都会引起渲染队列的强制执行:off-client-scroll-。为了减少回流和重绘,可以合理利用浏览器的优化策略,少去读取几何信息,或者统一放到最后读取。

其他方法

1. 样式集中改变:不要逐个修改样式,通过动态添加class。
代码简示

js 复制代码
document.getElementById('demo').className = 'demo'; // 统一添加/修改样式
css 复制代码
.demo {
  color: red;
  background: #ccc;
  padding: 15px 20px;
}

2. 离线操作DOM:不直接操作真实的节点。

  • none、block优化 :当对DOM节点有较大改动的时候,我们先将元素脱离文档流display:none(需要一次重排),然后对元素进行操作,最后再把操作后的元素放回文档流display:block(需要一次重排)。

代码简示

js 复制代码
  const renderEle = document.getElementById('demo');
  renderEle.style.display = 'none'; // 导致重排
  
  // DOM不存在渲染树上不会引起重排、重绘
  renderEle.style.color = 'red'; 
  renderEle.style.background= '#ccc';  
  renderEle.style.marginLeft = '15px';
  renderEle.style.marginTop = '15px';
  renderEle.style.border = '2px solid #ccc';
    
  renderEle.style.display = 'block';// 导致重排
  • Fragment文档碎片优化Fragment是一种机制,用于在内存中创建一个轻量级的文档碎片,这个文档碎片可以包含多个节点,它不涉及dom结构的实际插入,因此不会触发回流,最后带有批量节点的文档碎片插入到文档中,这样可以减少回流的次数。

代码简示

js 复制代码
const ul = document.getElementById("box");
const fragment = document.createDocumentFragment(); // 虚拟的文档片段

for (let i = 1; i<=100; i++) {
    let li = document.createElement("li");
    let text = document.createTextNode(i) 
    li.appendChild(text)
    fragment.appendChild(li); // 虚拟片段不会回流
}

ul.appendChild(fragment) 
  • cloneNode方法优化 :使用 cloneNode 方法进行深拷贝,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。

代码简示

js 复制代码
const ul = document.getElementById("box");
const clone = ul.cloneNode(true) // 克隆一份ul,必定是深拷贝,原ul不受影响

for (let i = 1; i<=100; i++) {
    let li = document.createElement("li");
    let text = document.createTextNode(i) // 创建文本节点
    li.appendChild(text)
    clone.appendChild(li);
}

ul.parentNode.replaceChild(clone, ul) // (替代品, 被替代品)

3. 脱离文档流:使用 absolute 或 fixed 脱离文档流。

4. 善用内存:在内存中多次操作DOM,再整个添加到DOM树 构建整个ul,而不是循环添加li
代码简示

循环添加li,每一次都会引起重排

html 复制代码
<div id="demo">
  <ul id="father">
    <li>我是0号,我后面还有1号、2号、3号、4号、5号</li>
  </ul>
</div>
js 复制代码
const ulEle = document.getElementById("father");
let arr = [];
setTimeout( () => {
  arr = "我是0号,我后面还有1号,2号,3号,4号,5号", "我是2号", "我是3号", "我是4号", "我是5号"]; // 我是动态获取的
  arr.forEach(element => {
    const childNode = document.createElement('li');
    childNode.innerText = element;
    ulEle.appendChild(childNode);// 每一次都会引起重排(重排会引起重绘)
  })
},1000)

构建整个ul,只引起一次重排

html 复制代码
<div id="demo"></div>
js 复制代码
const ulEle = document.getElementById("demo");
const childUlNode = document.createElement('ul');
let arr = [];
setTimeout(() => {
  arr = ["我是0号,我后面还有1号,2号,3号,4号,5号","我是1号", "我是2号", "我是3号", "我是4号", "我是5号"]; // 我是动态获取的
   arr.forEach(element => {
     const childLiNode = document.createElement('li');
     childLiNode.innerText = element;
     childUlNode.appendChild(childLiNode);
   })
},1000)
ulEle.appendChild(childUlNode);// 只会引起一次重排(重排会引起重绘)

参考文章

juejin.cn/post/732028...

juejin.cn/post/715915...

网页渲染性能优化(深)

相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss2 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247554 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255024 小时前
前端常用算法集合
前端·算法
真的很上进5 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203985 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2345 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1236 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~6 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语7 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js