浏览器中的重构与回流

原文:Browser | 重绘和回流
前言:回流(重排)和重绘是前端面试过程中经常会提到的一个知识点,所以想写一篇文章整理下相关的知识点。

要了解什么是回流(重排)与重绘,首先就要了解一下浏览器是如何进行画面渲染的。

浏览器是如何进行画面渲染的?

  • 解析(Parser)HTML,生成DOM树(Tree),解析(Parser)CSS,生成样式规则(Style Rules)
  • 根据刚才生成的DOM树与样式规则,生成渲染树(Render Tree)
  • 进行布局Layout(回流/重排):根据生成的渲染树,得到节点的集合信息(位置,大小)
  • 进行绘制Painting(重绘):根据计算和获取的信息进行整个页面的绘制
  • Display:最后将画面显示在页面上

重绘和回流(重排)

  • 回流:渲染树(Render Tree)中的元素的尺寸、结构、布局(几何属性)发生改变的时候,浏览器就会重新渲染部分或者全部文档的过程。
  • 重绘:对元素样式的改变并不影响它在文档流中的位置和文档布局(没有改变元素的几何属性),浏览器直接为该元素绘制新的样式。

回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流

如何触发

回流(重排)

  • 页面一开始加载的时候(无法避免)

  • 脚本操作DOM(增加或者删除可见的DOM元素)

  • 元素的几何属性发生变化(大小,位置)

  • 元素的内容发生了变化(如:input框中输入的内容,图片被另一个不同尺寸的图片所替代)

  • 激活css伪类(如:#div::hover

  • 字体的大小发生改变

  • 浏览器的窗口大小发生了改变(回流是根据视口的大小来计算元素的位置和大小的)

  • 使用一些特定的属性

    offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight

    这些属性是通过实时计算得到的,浏览器获取这些值的时候,也会进行回流的操作,使用getComputedStyle方法的时候同理

大部分时候可以认为,只要影响到页面布局就会有回流的发生。

重绘

  • 上文提到的回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流
  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

下面来看几个和重绘与回流相关的案例熟悉一下。

案例1:

javascript 复制代码
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));

案例2:

问题:display:nonevisibility:hidden会产生回流与重绘吗?

答:

  • display:none元素隐藏之后不占用文档流(在渲染树里面不存在节点),DOM树发生了变化,所以会引起重绘与回流。

  • visibility:hidden显示在页面上,但是隐藏元素仍需占用与未隐藏时一样的空间(在渲染树里面存在节点),没有影响到页面的结构,所以只有产生重绘。

补充:display: none 的子元素不会进行显示,而visibility: hidden的子元素却是可以进行设置显示的

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程

浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列.

当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据

因此浏览器不得不清空队列,触发回流重绘来返回正确的值。

如何避免触发回流与重绘

  1. 如果想通过js修改样式最好通过类(class)的方式去触发(补充:使用cssText也可以)
  2. 避免设置多项内联样式
  3. 批量修改dom时候通过以下思路减少回流与重绘的发生
    • 使元素脱离文档流
    • 对其进行多次修改
    • 将元素带回到文档中
  4. 避免触发同步布局事件,比如前文提到的需要实时读取的属性(如:offsetWidth),这样不需要每次循环的时候都读取一次
  5. 遇到复杂的动画效果,使用position: fixed/absolute让其脱离文档流,从而减少对其他元素的影响
  6. css3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘。
    • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
    • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

设定元素样式,最好通过类(class)的方式去触发

下面这种方式,在比较老的浏览器上每次赋值操作都会引起回流与重绘

❕注:比较新的浏览器会使用队列来储存多次修改,进行优化,所以在新的浏览器上只会触发一次重绘和回流

javascript 复制代码
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

为了避免触发重回与回流,可以使用类(class)来给元素触发对应的样式,下面就是优化过后的代码:

CSS:

css 复制代码
.active {
  width: 100px;
  height: 200px;
  border: 10px solid red;
  color: red;
}

JS:

javascript 复制代码
const el = document.querySelector('#container');
// 给元素追加一个类
el.classList.add('active');

补充:其实还可以使用cssText来给样式做重新赋值,但是这种方式写法看上去比较繁琐,所以了解一下就好了。对应的代码如下:

javascript 复制代码
const el = document.querySelector('#container');
// 通过cssText属性给元素添加样式
el.style.cssText = 'width: 100px;' + 
                  'height: 200px;' +
                  'border: 10px solid red;' +
                  'color: red;';

批量修改dom时候通过以下思路减少回流与重绘的发生

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中

该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了

有三种方式可以让DOM脱离文档流:

  • 隐藏元素,应用修改,重新显示
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
  • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

隐藏元素,应用修改,重新显示

javascript 复制代码
function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

uldisplay设置为none后,该元素就不存在与渲染树中了

使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

javascript 复制代码
const ul = document.getElementById('list');
// 使用createDocumentFragment创建一个新的空白的文档片段
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
// 将元素追加到ul元素中去
ul.appendChild(fragment);

DocumentFragments (en-US) 是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。

因为文档片段存在于内存中,并不在 DOM 树中 ,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。------(MDN-Document.createDocumentFragment()

将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

javascript 复制代码
const ul = document.getElementById('list');
// 对该节点进行克隆,参数1为true代表深度克隆
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
// 将旧的元素替换为修改后的元素
ul.parentNode.replaceChild(clone, ul);

避免触发同步布局事件

javascript 复制代码
// 将变量放在循环的外面,避免每次循环的时候都要重新读取,进而导致没必要的重绘与回流发生
const width = box.offsetWidth;
for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
}

css3硬件加速(GPU加速)

划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

注意:

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

参考文献

相关推荐
涔溪35 分钟前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
fakaifa2 小时前
CRMEB Pro版v3.1源码全开源+PC端+Uniapp前端+搭建教程
前端·小程序·uni-app·php·源码下载
夜色呦2 小时前
掌握ECMAScript模块化:构建高效JavaScript应用
前端·javascript·ecmascript