浏览器的重排与重绘

  1. 浏览器渲染原理
  2. 什么是重排(reflow)?
  3. 什么是重绘(repaint)?
  4. 通过例子分析重排和重绘
  5. 渲染树变化的排队与刷新
  6. 优化

浏览器渲染原理

重排和重绘是浏览器运行过程中一个很重要的特性,页面的动画,结构变化都会涉及到重排与重绘,重排与重绘也是影响前端页面性能的一个很重要的因素,如果页面存在大量的重排与重绘,页面会显得非常“卡”,影响用户体验。

说道重排与重绘,首先要了解浏览器的渲染原理,浏览器渲染展示页面的过程,大致分为以下几步:

  • 解析HTML结构(HTML Parser),构建DOM树DOM Tree
  • 解析CSSCSS Parser),构建CSS规则树Style Rules
  • DOM树CSS规则树合并,构建渲染树Rendering Tree
  • 布局(Layout)和绘制(Paint
  • 如果在渲染过程中发生了结构变化或者样式变化,则会进行重排(reflow)和重绘(repaint

重排(reflow)也称为回流

Webkit内核渲染引擎工作原理(Chrome,Safari,Opera)

Geoko内核渲染引擎工作原理(Firefox)

什么是重排(reflow)?

当页面布局完成后,由于用户操作,增删了节点,修改了节点的宽高等,浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow.

reflow(回流)是导致DOM脚本执行效率低的关键因素之一,页面上任何一个节点触发了reflow,会导致它的子节点及祖先节点重新渲染。

简单解释一下 Reflow:当元素改变的时候,将会影响文档内容或结构,或元素位置,此过程称为 Reflow。

当页面布局和几何属性改变时就需要重排。下述情况会发生浏览器重排:

  • 添加或者删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变(包括:内外边距、边框厚度、宽度和高度等属性的改变)
  • 内容改变,例如:文本改变或者图片被另一个不同尺寸的图片替代
  • 页面渲染器初始化
  • 浏览器窗口尺寸改变
  • 对可见元素 display:none,或者对不可见元素 display:block 时
  • 激活伪类(:hover)
  • transition对宽高的处理,在整个transition的每一帧中,浏览器都要去重新布局,绘制页面(参考)

什么是重绘(repaint)?

repaint是在一个元素的外观被改变,但没有改变布局的情况下发生的,如改变了visibilityoutlinebackground等。当repaint发生时,浏览器会验证DOM树上所有其他节点的visibility属性。

避免过分重绘(Repaints)
当元素改变的时候,将不会影响元素在页面当中的位置(比如 background-color, border-color, visibility),浏览器仅仅会应用新的样式重绘此元素,此过程称为 Repaint

render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的,比如 background-color,则称之为重绘。

  • 改变字体
  • 增加或者移除样式表
  • 内容变化,比如用户在input框中输入文字
  • 激活CSS伪类(:hover)
  • 脚本操作DOM (也有可能造成回流)
  • 计算 offsetWidth 和 offsetHeight 的属性
  • 设置style属性的值

通过例子分析重排和重绘

我们可以结合浏览器的性能分析工具,来看到浏览器的渲染过程,通过以下代码,我们来分别看看重排和重绘何时发生

<!DOCTYPE html>
<html lang="zh">
<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" />
    <title>Document</title>
    <style type="text/css">
        .main{
            border: 1px solid red;
        }
    </style>
</head>
<body>
  <button id="addNode">添加节点</button>
  <button id="changeStyle">修改颜色</button>
  <div class="main">
    <ul class="ul">
      <li>11111</li>
      <li>22222</li>
      <li>33333</li>
      <li>44444</li>
    </ul>
  </div>

  <script src="https://cdn.bootcss.com/zepto/1.2.0/zepto.min.js"></script>
  <script type="text/javascript">
    const ul = document.querySelector('.ul');
    const addNodeBtn = document.querySelector('#addNode');
    const changeStyleBtn = document.querySelector('#changeStyle');

    addNodeBtn.onclick = function addNode() {
      const newLi = document.createElement('li');
      newLi.innerText = Math.random() * 10000;
      ul.appendChild(newLi);
    }

    changeStyleBtn.onclick = function changeStyle() {
      $('.main').css('color', 'red')
    }
  </script>
</body>
</html>

当我们向ul中添加一个li,此时dom结构发生了变化,会发生重排

当我们仅仅只是修改了文字的颜色,此时并不会发生重排,仅仅会发生重绘

同时我们可以看到,只要发生了重排,则必然会出现重绘,而重绘并不一定会重排,我们可以得出一个结论:重排必定会引发重绘,但重绘不一定会引发重排。

渲染树变化的排队与刷新

浏览器会维护一个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。然而你可能会(经常不知不觉)强制刷新队列并要求计划任务立即执行。获取布局信息的操作会导致队列刷新,比如以下方法:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight
  • scrollTopscrollLeftscrollWidthscrollHeight
  • clientTopclientLeftclientWidthclientHeight
  • widthheight
  • getComputedStyle() (currentStyle in IE)
  • JS更改元素style

以上属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理”变化并触发重排以返回正确的值。

在修改样式的过程中,最好避免使用上面列出的属性。它们都会刷新渲染队列,即使你是在获取最近未发生改变的或者与最新变化无关的布局信息。

优化

上述提到的浏览器自己的优化,维护一个队列,队重排和重绘进行批处理

开发者需要注意的优化

  • 直接改变元素的className
  • display:none; 先设置元素为display:none;,然后进行页面布局等操作;设置完成后将元素设置为display:block;,这样的话就只引发两次重绘和重排;
  • 不要经常访问浏览器的flush队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;
  • 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;
  • 将需要多次重排的元素,position属性设为absolutefixed,元素脱离了文档流,它的变化不会影响到其他元素;
  • 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document
  • 尽量不要使用table布局。
  • 制作动画时,尽量使用 CSS3transform,因为 transform 属性不会改变元素的布局(更详细的知识可以参考:详谈层合成composite

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com