回流和重绘

6,765 阅读9分钟

摘要

回流和重绘是前端开发的高频词汇之一,你可以在各种面经,性能优化相关文章可以看到,但是很多都是草草带过。 本文带你了解回流与重绘的原理。

什么是回流?

当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。【重新排列布局,即打碎重组】

由本身的大小宽高改变,引发 局部全局 的排版,会引发回流或局部回流

  • 全局范围:从根节点 html 开始对整个渲染树进行重新布局。
  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

什么是重绘?

当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。这个过程叫做重绘。

只改变外观、风格,不影响布局,会引发重绘

回流比重绘大

大,在这个语境里的意思是:谁能影响谁?

  • 重绘:某些元素的外观被改变,例如:元素的填充颜色
  • 回流:重新生成布局,重新排列元素。

局部范围重排:

用局部布局来解释这种现象:把一个 dom 的宽高之类的几何信息定死,然后在 dom 内部触发重排,就只会重新渲染该 dom 内部的元素,而不会影响到外界。 就如上面的概念一样,单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分。

一句话概括:回流必将引起重绘,重绘不一定会引起回流


浏览器渲染流程

浏览器的主要功能就是向服务端发送请求,下载解析资源显示在浏览器上。将网页内容展示到浏览器上的过程,这其实就是渲染引擎完成的。渲染引擎有很多种,这里以 webkit(chrome) 为例。

渲染流程

从上面这个图上,我们可以看到,浏览器渲染流程如下:

  • 解析 HTML Source,生成 DOM 树。
  • 解析 CSS,生成 CSSOM 树。
  • 将 DOM 树和 CSSOM 树结合,去除不可见元素(很重要),生成渲染树( Render Tree )。
  • Layout (布局):根据生成的渲染树,进行布局( Layout ),得到节点的几何信息(宽度、高度和位置等)。
  • Painting (重绘):根据渲染树以及回流得到的几何信息,将 Render Tree 的每个像素渲染到屏幕上。

渲染树

渲染树

构建渲染树流程:

  • 从 DOM 树的根节点开始遍历每个可见节点。
  • 对于每个可见的节点,找到 CSSOM 树中对应的规则,并应用它们。
  • 根据每个可见节点以及其对应的样式,组合生成渲染树。

什么是不可见节点

  • 一些不会渲染输出的节点,比如 script、meta、link 等。
  • 一些通过 css 进行隐藏的节点。比如 display : none。注意,使用 visibility 和 opacity 隐藏的节点,还是会显示在渲染树上的(因为还占据文档空间),只有 display : none 的节点才不会显示在渲染树上

css 加载是否会阻塞 dom 树渲染

这里说的是头部引入 link:css 的情况

首先,我们都知道:css 是由单独的下载线程异步下载的。

咱们先分析下 css 加载会影响什么,刚才的问题太笼统了,咱们需要细化一下。

会影响什么呢?

  • 一个就是 DOM 树解析
  • 一个就是构建渲染树【render 树】

假设都不影响

这个时候你加载 css 的时候,很可能会修改下面 DOM 节点的样式,如果 css 加载不阻塞 render 树渲染的话,那么当 css 加载完之后, render 树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。

所以这个假设是不成立。

假设都影响

前提 --- 了解 dom 树的解析和构建,本来 dom 树的解析和构建这一步和 css 还没有关系,所以根本谈不上影响吧。 而且是两个不同的线程,各忙各的呗。(有上文得到成立)

所以干脆就先把 DOM 树的结构先解析完,把可以做的工作做完,然后等你 css 加载完之后, 在根据最终的样式来渲染 render 树,这种做法性能方面确实会比较好一点。

我得出结论 css 加载不会阻塞 DOM 树解析(异步加载时 DOM 照常构建),但会阻塞 render 树渲染(渲染时需等 css 加载完毕,因为 render 树需要 cssom 信息)

那么 js 的 script 和 css @import 会不会堵塞?后续。。。


在深入一亿点回流

注意

回流需要更新渲染树,性能花销非常大,它们的代价是高昂的,会破坏用户体验,并且让 UI 展示非常迟缓,我们需要尽可能的减少触发重排的次数。 回流的性能花销跟渲染树有多少节点需要重新构建有直接关系,所以我们应该尽量以局部布局的形式组织 DOM 结构,尽可能小的影响重排的范围,而不是像全局范围的示例代码一样一溜的堆砌标签,因为随便一个元素触发重排都会导致全局范围的重排。

常见引起回流的设计

  1. 添加或者删除可见的 DOM 元素;
  2. 元素尺寸改变——边距、填充、边框、宽度和高度;
  3. 浏览器窗口尺寸改变——resize 事件发生时
  4. 计算 offsetWidth 和 offsetHeight 属性

常见引起重排属性和方法

  • width
  • height
  • margin
  • padding
  • display
  • border
  • position
  • overflow
  • clientWidth
  • clientHeight
  • clientTop
  • clientLeft
  • offsetWidth
  • offsetHeight
  • offsetTop
  • offsetLeft
  • scrollWidth
  • scrollHeight
  • scrollTop
  • scrollLeft
  • scrollIntoView()
  • scrollTo()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollIntoViewIfNeeded()

在深入一亿点重绘

常见引起重绘属性和方法

  • color
  • border-style
  • visibility
  • background
  • text-decoration
  • background-image
  • background-position
  • background-repeat
  • outline-color
  • outline
  • outline-style
  • border-radius
  • outline-width
  • box-shadow
  • background-size

浏览器的渲染队列

思考以下代码将会触发几次渲染?

div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";

根据我们上文的定义,这段代码理论上会触发 4 次(重排/重绘),因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制:

当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。(类似 tick)

div.style.left = "10px";
console.log(div.offsetLeft);
div.style.top = "10px";
console.log(div.offsetTop);
div.style.width = "20px";
console.log(div.offsetWidth);
div.style.height = "20px";
console.log(div.offsetHeight);

这段代码会触发 4 次重排+重绘,因为在 console 中你请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。 因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。


优化建议

读写分离操作

div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";
// 分离引用读取
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

样式集中操作

div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";

虽然现在大部分浏览器有渲染队列优化,不排除有些浏览器以及老版本的浏览器效率仍然低下:建议通过改变 class 或者 csstext 属性集中改变样式

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// good
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
el.style.cssText += `; left:${left}px; top:${top}px;`;

缓存布局信息

// bad
div.style.left = div.offsetLeft + 1 + "px";
div.style.top = div.offsetTop + 1 + "px";

// good 缓存布局信息 相当于读写分离 ;想深入了解缓存优化参考 《小鹦鹉》
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + "px";
div.style.top = curTop + 1 + "px";
curLeft = curTop = null;

"离线"改变 DOM

  1. 隐藏要操作的 dom 在要操作 dom 之前,通过 display 隐藏 dom,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘

    dom.display = "none";
    // 修改 dom 样式
    dom.display = "block";
    
  2. 通过使用文档碎片创建一个 dom 碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排。 或者复制节点,在副本上工作,然后替换它!

避免过分重绘

浏览器仅仅会应用新的样式重绘此元素。

position 属性为 absolute 或 fixed

position 属性为 absolute 或 fixed 的元素,重排开销比较小,不用考虑它对其他元素的影响

优化动画

动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以 1 个像素为单位移动这样最平滑,但是回流就会过于频繁,大量消耗 CPU 资源;

举例优化如果以 3 个像素为单位移动,则会好很多。

启用 GPU 加速

GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。 GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3 转换(transitions),CSS3 3D 变换(transforms),WebGL 和视频(video)。

/*
 * 根据上面的结论
 * 将 2d transform 换成 3d
 * 就可以强制开启 GPU 加速
 * 提高动画性能
 */
div
  transform translate3d(10px, 10px, 0)
/**
水平垂直居中利用 GPU 加速
*/
.box2
  background black
  width 100px
  height 100px
  position absolute
  top 50%
  left 50%
  transform translate3d(-50%,-50%,0)