理解 Web 的重排和重绘

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

Web 中 的重排和重绘是 Web 渲染中常见的问题,即 Relayout(重排)和 Repaint(重绘)。其中重排(Relayout)也常被称为回流(Reflow)。另外在 Web 中聊渲染相关的话题,除了 Repaint、Reflow、Relayout之外,还有 Restyle(重写样式)和 Rendering(渲染)。这五个带有 R 开头的单词简称 5R ,对于 Web 性能的优化有着决定性的影响。这五个 R 分别是:

  • Rendering :渲染
  • Repaint:重绘
  • Reflow: 回流(也称重排)
  • Relayout:重新布局 (和 Reflow 表达相同的意思)
  • Restyle :重新设计(重写样式)

如果要提高页面的渲染性能就需要深究造成 Repaint 和 Reflow 的相关原因。

浏览器渲染过程

用下图来阐述浏览器是如何工作的:

  • 浏览器引擎:将 HTML 文档和页面的其他资源转换为用户设备上的互动视觉表现(Interactive Visual Representation)
  • 布局引擎 和 渲染引擎:理论上,布局和渲染(或“绘制”)可以由独立的引擎处理;事实他们是紧密耦合在一起,很少单独考虑

当你在某个链接或 URL 上敲击回车键时,浏览器就会向该页面发起 HTTP 请求,相应的服务器则提供 (通常)HTML 文档作为回应(当然,这中间会发生很多事情)。

不同的浏览器的处理过程并不一致,但一定会有大家都会遵从统一的地方,下面这张图就把这个过程作了总结,各家的浏览器接收到代码之后,多多少少都会按照下面这个步骤将代码转化成显示器上的网页:

  • 首先解析 HTML 代码,将其所有转化成 DOM 树
    • 每一个 HTML 标签对应一个 节点(Node)
    • HTML 标签(元素)的内容则被转换成 文本节点(Text Node)
    • 树形的根部(Root Node)则是 documentElement<html>元素)
  • 碰到 <link rel="stylesheet"><style> 开始处理 CSS 样式表,会将 CSS 样式表解析成 CSSOM 树
    • 浏览器遇到自己不认识的前缀定义的样式规则,会直接被忽略
    • 样式信息层层递进,即层叠规则
      • 浏览器都有自己的默认样式 User Agent 样式
      • 用户设置样式
      • 开发者编写的样式
  • DOM 和 CSSOM 结合在一起,构建一个渲染树(Render Tree)
    • 渲染树有点像 DOM 树,但并不完全匹配
    • 渲染树了解样式
    • 可能有一些DOM元素在渲染树中有不止一个节点
    • 渲染树中的一个节点被称为一个帧(Frame),也称为盒子(Box,对应的就是 CSS 盒模型)
    • 每个节点都有 CSS 盒模型的属性
  • 渲染树构建完成,浏览器就可以在屏幕上绘制(画出)渲染树的节点

在这些树(DOM 树、CSSOM树,Render树)上都有相应的节点,而这些节点都是有对应的映射转换:

浏览器解析 Web 页面更详细的介绍,可以阅读《初探 CSS 渲染引擎》。

重排和重绘的概念

页面的生命周期中

网页生成的时候至少会有一次渲染。在用户的访问过程中,还会触发重排(Reflow)和 重绘(Repaint)

正如上图所示,在页面整个生命周期中,除了首次渲染之外,后面会随着一些操作,比如 JavaScript 脚本动态操作 DOM 或 CSSOM,用户的输入(比如在文本框中输入内容,点击按钮或鼠标悬浮在按钮上),异步加载,动效,用户滚动页面以及用户调整浏览器视窗大小等,都会在首次渲染的基础上进行更新(会有Reflow和Repaint)。

简单地说,在浏览器打开任何一个页面的时候,都会进行一次绘制。在此之后,对构建渲染树的信息进行任何改变都会造成以下一种或者两种结果:

  • 渲染树的部分(或者全部)内容需要重新验证,并重新计算节点的尺寸。这个过程被称为 回流(Reflow)布局(Layout,或 Layouting)重排(Relayout)! 重点是,页面的初始布局(Layout)至少会回流(Reflow)一次
  • 页面中的部分内容需要获得更新,比如一些节点(Node)的几何(Geometry)信息变化,或者一些类似背景颜色之类的CSS样式上的变化。这个过程被叫作 重绘(Repaint)或 重画(Redraw)

不管页面发生了重绘还是重排,都会影响性能,最可怕的是重排,因为:重绘不一定导致重排,但重排一定会导致重绘!

重绘

当元素(节点)的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程叫做 重绘(Repaint)

浏览器通过构造渲染树和回流阶段,可以知道哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小等),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。简单地说,重绘是填充像素的过程。它涉及绘出文本、颜色、图像、边框 和阴影,基本上包括元素的每个可视部分。在重绘阶段,系统会遍历渲染树,并调用渲染对象的 paint 方法,将渲染对象的内容显示在屏幕上。

从上图可以看出,如果修改了元素的背景颜色,那么布局阶段将不会执行,因为并没有引起几何位置的变换,所以就直接进入绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作高一些。

浏览器将渲染对象的内容绘制到屏幕会按照一定的绘制顺序进行绘制,这个绘制顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块渲染对象的堆栈顺序是:背景颜色 → 背景图片 → 边框 → 内容 → 轮廓

重排(Reflow/Relayout)

当 DOM 的变化影响了元素的几何信息(DOM 对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中正确位置,这个过程叫做 重排(Relayout) 也称作 回流(Reflow)

重排是浏览器中执行的一个流程,用于重新计算文档中各元素的位置和几何形状,以便重新渲染该文档的部分内容或全部内容。由于重排会阻止用户在浏览器中执行操作,因此开发者需要了解如何优化重排,以及各种文档属性(DOM 深度、CSS规则效率和不同类型的样式更改)对重排用时的影响。有时,对文档中的单个元素进行重排可能需要同时对其父元素及其后面的所有元素进行重排。

简单地说,更新了DOM元素的几何属性,就会发生重排:

从上图可以看出,如果你通过 JavaScript 脚本 或 CSS 修改元素的几何位置属性,浏览器就会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。重排需要更新完整渲染流水线,所以开销也是最大的。

重排是一个非常昂贵的操作,大部分重排都会导致页面被重新渲染(重绘)。它是导致 DOM 脚本执行缓慢的主要原因之一,特别是在移动手机端。在大部分情况下,重排等同于生新进入一次页面!

重排重绘与布局计算

  • 浏览器如何渲染文件
    • 用户点击一链接或输入URL按下Enter键时,会接收来自服务器的数据(字节)
    • 解析字节并转换为令牌(Token):
      • 标签起始符:<
      • 标签名称:TagName
      • 标签属性: Attribute
      • 标签属性值: AttributeValue
      • 标签结束符:>
    • 将令牌(Token)转换为节点(Node)
    • 将节点转换为 DOM 树
    • 从 CSS 规则中创建 CSSOM 树 (解析 HTML时碰到 <link rel="stylesheet"><style> 开始解析 CSS)
    • 将 CSSOM 树和 DOM 树合并为 渲染树(RenderTree)
      • 计算哪些元素是可见(visible)的以及它们的计算样式(Computed Styles)
      • 从 DOM 树的 根(<html>)开始
      • 不可见的元素(如,<meta><link><script> ) 和 display: none 不会挂到渲染树上(被渲染树忽略)
      • 对于每个可见的节点,找到与其匹配的 CSSOM规则,并运用到节点上
    • 布局(Layout 或 Reflow):计算每个可见元素的布局(位置和几何尺寸)
    • 绘制(Paint 或 Repaint):将像素渲染到屏幕上
  • 重绘(Repaint)
    • 可见性发生变化时会触发,比如 opacitycolorbackground-colorvisibility
  • 重排(Reflow,Relayout,Layout, LayoutFlush, LayoutThrashing)
    • 变化影响到布局,比如widthpositionfloat
    • 重新计算位置和尺寸
    • 有更大的影响:
      • 改变一个元素会影响到所有的子元素、祖先元素和同级元素或整个文档, 比如改变 DOM 或 CSS,滚动,用户行为(如获取焦点)
    • 只有当文档发生变化并使布局无效时,回流才会有代价
    • 无效的东西 + 触发的东西 = 昂贵的回流

而布局计算就发生在 RenderObject 树的每个 RenderObject 对象上的,属于重排的其中一个环节。

DOM树解析完成的时机,调用了 UpdateStyleAndLayoutTree() 方法触发 LayoutTree 的更新。

网页加载之后,每当浏览器需要重新绘制新的一帧的时候,一般需要三个阶段:计算布局 、绘制 和 合成

这三个阶段越少,页面的性能就越好:

在页面初始化的整个渲染周期中,布局计算(Layout)绘制(Paint) 是最耗时的两个阶段,最后一个阶段 合成(Composite) 是较快的。而每次的布局计算后,一旦布局发生了改变,后续的绘制操作也会接着进行。因此,我们有必要了解在页面渲染周期中,哪些情况是需要重新计算布局:

  • 网页的可视区域(Viewport) 发生变化时都需要重新计算布局
  • 页面中的动画会触发布局计算。如果动画需要改变元素的一些大小或尺寸,那么就需要重新进行布局计算
  • JavaScript 脚本修改样式信息
  • 用户的交互也会触发布局计算,比如滚动页面,会触发新区域布局计算

简单地来说,只要样式发生了变化,都需要进行重新计算。

布局计算根据其范围大致分为两类:

  • 对整个 RenderObject 树进行计算
  • RenderObject 树中某个子树的计算

布局计算是一个递归的过程,这是因为一个节点的大小通常需要先计算它的子节点的位置、大小信息才能被确定。布局计算是以包含块和盒子模型为基础的,元素的布局计算都依赖于块,而它们通常是在垂直方向上展开的。

下图为布局计算过程描述:

重绘和重排是昂贵的

先来看实际案例的渲染,下面三个案例都是使用 JavaScript 脚本动态创建内容(20000次):

  • 案例1: 每次都访问,并修改 DOM + 重排 + 重绘
  • 案例2: 多次访问 DOM,一次修改 DOM + 重排 + 重绘
  • 案例3: 一次访问,并修改 DOM + 重排 + 重绘

    Case1
        <script>
            var times = 20000;
    
            // Case1: 每次都访问并修改DOM + 重排 + 重绘
            console.time(1);
            for(var i = 0; i < times; i++) {
                document.getElementById('container').innerHTML += i + '<br/>'  
            }
            console.timeEnd(1);
    
        </script>
    </body>
    

Chrome浏览器耗时: 1: 458966.73193359375 ms。(无痕耗时: 388878.5646972656 ms

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Case2</title>
    </head>
    <body>
        <div id="container"></div>
        <script>
            var times = 20000;

            // Case2: 多次访问 DOM, 一次修改 DOM + 重排 + 重绘
            console.time(1);
            var str = ''
            for(var i = 0; i < times; i++ ) {
                var tmp = document.getElementById('container').innerHTML
                str += i + '<br/>'  
            }
            document.getElementById('container').innerHTML = str
            console.timeEnd(1)
        </script>
    </body>
</html>

Chrome 浏览器耗时: 34.890869140625 ms (无痕耗时:36.68994140625 ms

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Case3</title>
    </head>
    <body>
        <div id="container"></div>
        <script>
            var times = 20000;

            // Case3: 一次访问并修改 DOM + 重绘 + 重排
            console.time(1)
            var str = '';
            for (var i = 0; i < times; i++) {
                str += i + '<br/>'  
            }
            var container = document.getElementById('container') 
            container.innerHTML = str
            console.timeEnd(1)
        </script>
    </body>
</html>

Chrome耗时:30.361083984375 ms (无痕:33.60205078125 ms

剩余80%内容付费后可查看

如需转载,烦请注明出处:https://www.w3cplus.com/performance/repaint-and-reflow.html

如果文章中有不对之处,烦请各位大神拍正。如果你觉得这篇文章对你有所帮助,打个赏,让我有更大的动力去创作。(^_^)。看完了?还不过瘾?点击向作者提问!

赏杯咖啡,鼓励他创作更多优质内容!
返回顶部