前端面试 第四篇 重绘和重排(回流)
前端八股文 怎么能不懂一点呢~ 这是前端面试 最喜欢问的问题, 让我们一起看一下~
前端在做UI交互效果的时候会不会存在这样一个质疑:这样到底做好不好❓
主要是两个方面导致质疑的存在(~~嘿嘿,实际上是想更优秀😜)
:
- 一个是页面布局方面(此处不深入哦~);
- 一个是浏览器渲染方面(会不会触发重绘和重排❓)
此时的你可能会有些疑问:
-
什么是浏览器的重绘重排❓
-
怎么在实际项目中把重绘重排引起的性能问题考虑进去❓
-
怎么权衡重绘与重排❓
以下可以部分解释以上三个疑问:
重绘 和 重排(回流)
重绘不一定导致重排,但重排一定会导致重绘 重排(Reflow) && 重绘(Redraw)会付出高昂的性能代价
重绘
重绘 (Redraw
):某些元素的外观被改变所触发的浏览器行为(重新计算节点在屏幕中的绝对位置并渲染的过程); 例如:修改元素的填充颜色,会触发重绘;
重排(回流)
重排 (Reflow
):重新生成布局,重新排列元素(重新计算各节点和css具体的大小和位置:渲染树需要重新计算所有受影响的节点**);例如:改元素的宽高,会触发重排;
通过两者概念区别明显得知,重排要比重绘的成本大得多,我们应该尽量减少重排操作,减少页面性能消耗
那些操作会导致重绘与重排 ❓
下面情况会发生重绘:
- color
- border-style
- border-radius
- text-decoration
- box-shadow
- outline
- background
- ...
下面情况会发生重排:
- 页面初始渲染,这是开销最大的一次重排;
- 添加/删除可见的DOM元素;
- 改变元素位置;
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
- 改变元素内容,比如文字数量,图片大小等;
- 改变元素字体大小;
- 改变浏览器窗口尺寸,比如resize事件发生时;
- 激活CSS伪类(例如::hover);
- 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;
- 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用getComputedStyl方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”;
- ...
重排影响的范围:
- 全局范围(全局布局):从根节点html开始对整个渲染树进行重新布局;
- 局部范围(局部布局):对渲染树的某部分或某一个渲染对象进行重新布局;
优化建议:
核心观念: 减少重排次数和减小重排范围
样式集中改变(减少重排次数
):
<!-- html -->
<span id="demo">
我是demo
</span>
// javascript
// renderEle.style 逐个添加/修改属性值
const renderEle = d、ocument.getElementById('demo');
renderEle.style.color = 'red'; // 导致重绘
renderEle.style.background= '#ccc'; // 导致重绘
renderEle.style.padding = '15px 20px'; // 导致重排(重排会引起重绘)
以上操作会导致 3次重绘 1次重排; 可以动态添加class,只会导致1次重排(重排会引起重绘),从而减少重绘次数; 可以合并为:
// javascript
document.getElementById('demo').className = 'demo'; // 添加class 统一添加/修改样式
/* css */
.demo {
color: red;
background: #ccc;
padding: 15px 20px;
}
将 DOM 离线(减少重排次数
)
离线操作DOM
:当对DOM 节点有较大改动的时候,我们先将元素脱离文档流,然后对元素进行操作,最后再把操作后的元素放回文档流。
1. 修改DOM节点的display属性,临时将此节点从文档流中脱离,然后再恢复;
<!-- html -->
<span id="demo">
我是demo
</span>
需要频繁操作DOM 修改style
// javascript
// 第一次操作修改 color、background、padding
const renderEle = document.getElementById('demo');
renderEle.style.color = 'red'; // 导致重绘
renderEle.style.background= '#ccc'; // 导致重绘
renderEle.style.padding = '15px 20px'; // 导致重排(重排会引起重绘)
// ...
// 第二次操作修改 marginLeft、marginTop
const renderEle = document.getElementById('demo');
renderEle.style.marginLeft = '15px'; // 导致重排(重排会引起重绘)
renderEle.style.marginTop = '15px'; // 导致重排(重排会引起重绘)
// ...
// 第三次操作修改 border
const renderEle = document.getElementById('demo');
renderEle.style.border = '2px solid #ccc'; // 导致重排(重排会引起重绘)
以上操作触发多次重排、重绘;
可以将renderEle
进行离线操作;
修改如下:
// javascript
const renderEle = document.getElementById('demo');
// 第一次操作修改 color、background、padding
renderEle.style.display = 'none'; // 导致重排(重排会引起渲)
renderEle.style.color = 'red'; // DOM不存在渲染树上不会引起重排、重绘
renderEle.style.background= '#ccc';// DOM不存在渲染树上不会引起重排、重绘
renderEle.style.padding = '15px 20px';// DOM不存在渲染树上不会引起重排、重绘
// ...
// 第二次操作修改 marginLeft、marginTop
renderEle.style.marginLeft = '15px';// DOM不存在渲染树上不会引起重排、重绘
renderEle.style.marginTop = '15px';// DOM不存在渲染树上不会引起重排、重绘
// ...
// 第三次操作修改 border
renderEle.style.border = '2px solid #ccc';// DOM不存在渲染树上不会引起重排、重绘
renderEle.style.display = 'block';// 导致重排(重排会引起渲)
以上对隐藏的DOM元素操作不会引发其他元素的重排,这样只在隐藏和显示时触发2次重排。
脱离文档流: 使用 absolute 或 fixed 脱离文档流(减小重排范围
):
<!-- html -->
<div id='demo'>
<span id="demo-one">
我是demo 1号
</span>
<span id="demo-two">
我是demo 2号
</span>
<span id="demo-there">
我是demo 3号
</span>
</div>
// javascript
const renderEle = document.getElementById('demo-one');
renderEle.style.padding = '15px 20px'; // 导致重排(重排会引起重绘)
renderEle.style.height = '60px'; // 导致重排(重排会引起重绘)
将需要重排的元素,position属性设为absolute或fixed(某些特殊场合),减小重排范围。
// javascript
const renderEle = document.getElementById('demo-one');
renderEle.style.position = 'fixed'; // 导致重排(重排会引起重绘)
renderEle.style.padding = '15px 20px'; // 导致重排(只有当前元素)
renderEle.style.height = '60px'; // 导致重排(只有当前元素)
这样此DOM元素就脱离了文档流,它的变化不会影响到其他元素。
善用内存:在内存中多次操作DOM,再整个添加到DOM树(减小重排范围
)
举例:异步请求接口获取数据,动态渲染到页面
<!-- html -->
<div id="demo">
<ul id="father">
<li>我是0号,我后面还有1号、2号、3号、4号、5号</li>
</ul>
</div>
// javascript
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,而不是循环添加li):
<!-- html -->
<div id="demo"></div>
// javascript
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);// 只会引起一次重排(重排会引起重绘)
读写分离:将写入的值缓存,读取缓存的值(减少重排次数
)
有一些浏览器针对重排做出来优化。 比如Opera:当你触发重排的条件到达一定量的时候, 或者等到一定时间的时候,或者等一个线程结束,再一起进行重排;但除了渲染树的直接变化,当获取一些属性时,浏览器为取得正确的值也会触发重排。这样就使得浏览器的优化失效了;
<!-- html -->
<span id="demo">
我是demo
</span>
// javascript
const offsetWidth = '100px';
const renderEle = document.getElementById('demo');
renderEle.style.offsetWidth = offsetWidth // 导致重绘(写入)
const tempoOffsetWidth = renderEle.style.offsetWidth // 读取可能会导致重排
上述代码中可使用读写分离(写入值的时候进行缓存),避免多次重排;
// javascript
const offsetWidth = '100px';
const renderEle = document.getElementById('demo');
renderEle.style.offsetWidth = offsetWidth // 导致重绘(写入)
const tempoOffsetWidth = renderEle; // 避免直接读取offsetWidth
真的不能再加班了,要跑路了,内卷太可怕,消耗不起了,兄弟们 在下告辞!!!
转载自:https://juejin.cn/post/7159155955987382309