likes
comments
collection
share

前端面试 第四篇 重绘和重排(回流)

作者站长头像
站长
· 阅读数 18

前端八股文 怎么能不懂一点呢~ 这是前端面试 最喜欢问的问题, 让我们一起看一下~

前端在做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

前端面试  第四篇  重绘和重排(回流) 真的不能再加班了,要跑路了,内卷太可怕,消耗不起了,兄弟们 在下告辞!!!