# 了解一下渲染原理

浏览器的渲染原理大致都差不多,下面是 Chrome 浏览器的渲染流程

image-20211112103214132

# 关键渲染路径

关键渲染路径指的是浏览器将HTMLCSSJS转化为实际运作的网站的必须采取的一系列步骤

  1. 渲染进程的主线程处理 HTML 并构建 DOM Tree(文档对象模型树)
  2. 接下来处理 CSS 并构建CSSOM Tree(CSS 对象模型树)
  3. 浏览器的布局系统根据 DOM TreeCSSOM Tree 生成 (布局树)Layout Tree(以前叫 Render Object Tree)
  4. 构建(图层树) Layer Tree 以便用正确的顺序展示页面,这棵树的生成与 Layout Tree 的构建同步进行
  5. 生成绘制列表,渲染进程中的主线程合成线程发送 commit 消息,把绘制列表交给合成线程
  6. 合成线程会根据绘制列表绘制图层

较为专业的术语

  1. 构建 DOM 树(DOM Tree)
  2. 样式计算(CSSOM Tree)
  3. 布局阶段(Layout Tree)
  4. 分层(Layer Tree)
  5. 绘制(Paint)
  6. 分块(Tile)
  7. 光栅化
  8. 合成

想象一下,从 0,1 字节流到最后页面展现在你面前,这里面渲染机制肯定很复杂,所以渲染模块把执行过程中化为很多的子阶段,渲染引擎从网络进程拿到字节流数据后,经过这些子阶段的处理,最后输出像素,这个过程可以称为渲染流水线 ,我们从一张图上来看

image-20211112103219819

# DOM Tree

DOM(Document Object Model——文档对象模型)是用来呈现以及与任意 HTML 或 XML 交互的 API 文档。DOM 是载入到浏览器中的文档模型,它用节点树的形式来表现文档,每个节点代表文档的构成部分。

需要说明的是 DOM 只是构建了文档标记的属性和关系,并没有说明元素需要呈现的样式,这需要 CSSOM 来处理。

# 构建流程

获取到 HTML 字节数据后,会通过以下流程构建 DOM Tree:

字节 → 字符 → 令牌 → 节点 → 对象模型(DOM)

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11

image-20211112103224957

  1. 编码:浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符
  2. 词法分析(标记化):浏览器对输入字符串进行逐字扫描,根据 构词规则 识别单词和符号,分割成一个个我们可以理解的词汇(学名叫 Token )的过程。
  3. 语法分析(解析器):对 Tokens 应用 HTML 的语法规则,进行配对标记确立节点关系绑定属性等操作,从而构建 DOM Tree 的过程。

词法分析和语法分析在每次处理 HTML 字符串时都会执行这个过程,比如使用 document.write 方法。

image-20201209230559302

# 词法分析(标记化)

HTML 结构不算太复杂,大部分情况下识别的标记会有开始标记、内容标记和结束标记,对应一个 HTML 元素。除此之外还有 DOCTYPE、Comment、EndOfFile 等标记。

标记化是通过状态机来实现的,状态机模型在 W3C 中已经定义好了。

想要得到一个标记,必须要经历一些状态,才能完成解析。我们通过一个简单的例子来了解一下流程。

<a href="www.w3c.org">W3C</a>
1

image-20201209230619688

  • 开始标记:
  • Data state:碰到 <,进入 Tag open state
  • Tag open state:碰到 a,进入 Tag name state 状态
  • Tag name state:碰到 空格,进入 Before attribute name state
  • Before attribute name state:碰到 h,进入 Attribute name state
  • Attribute name state:碰到 =,进入 Before attribute value state
  • Before attribute value state:碰到 ",进入 Attribute value (double-quoted) state
  • Attribute value (double-quoted) state:碰到 w,保持当前状态
  • Attribute value (double-quoted) state:碰到 ",进入 After attribute value (quoted) state
  • After attribute value (quoted) state:碰到 >,进入 Data state,完成解析
  • 内容标记:W3C
  • Data state:碰到 W,保持当前状态,提取内容
  • Data state:碰到 <,进入 Tag open state,完成解析
  • 结束标记:
  • Tag open state:碰到 /,进入 End tag open state
  • End tag open state:碰到 a,进入 Tag name state
  • Tag name state:碰到 >,进入 Data state,完成解析

通过上面这个例子,可以发现属性是开始标记的一部分。

# 语法分析(解析器)

在创建解析器后,会关联一个 Document 对象作为根节点

我会简单介绍一下流程,具体的实现过程可以在 Tree construction 查看。

解析器在运行过程中,会对 Tokens 进行迭代;并根据当前 Token 的类型转换到对应的模式,再在当前模式下处理 Token;此时,如果 Token 是一个开始标记,就会创建对应的元素,添加到 DOM Tree 中,并压入还未遇到结束标记的开始标记栈中;此栈的主要目的是实现浏览器的容错机制,纠正嵌套错误,具体的策略在 W3C 中定义。更多标记的处理可以在 状态机算法中查看。

# CSSOM Tree

# 样式计算

这个子阶段主要有三个步骤

  • 格式化样式表
  • 标准化样式表
  • 计算每个 DOM 节点具体样式

# 格式化样式表

我们拿到的也就是 0,1 字节流数据,浏览器无法直接去识别的,所以渲染引擎收到 CSS 文本数据后,会执行一个操作,转换为浏览器可以理解的结构-styleSheets

如果你很想了解这个格式化的过程,可以好好去研究下,不同的浏览器可能在 CSS 格式化过程中会有所不同,在这里就不展开篇幅了。

通过浏览器的控制台document.styleSheets可以来查看这个最终结果。通过 JavaScript 可以完成查询和修改功能,或者说这个阶段为后面的样式操作提供基石。

image-20201210150959740

# 标准化样式表

什么是标准化样式表呢?先看一段 CSS 文本 👇

body {
  font-size: 2em;
}
p {
  color: blue;
}
span {
  display: none;
}
div {
  font-weight: bold;
}
div p {
  color: green;
}
div {
  color: red;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

有些时候,我们写 CSS 样式的时候,会写font-size:2em;color:red;font-weight:bold,像这些数值并不容易被渲染引擎所理解,因此需要在计算样式之前将它们标准化,如em->px,red->rgba(255,0,0,0),bold->700等等。

上面的代码标准后属性值是什么样子呢

image-20211112103249771

# 计算每个 DOM 节点具体样式

通过格式化标准化,接下来就是计算每个节点具体样式信息了。

计算规则:继承层叠

继承:每个子节点会默认去继承父节点的样式,如果父节点中找不到,就会采用浏览器默认的样式,也叫UserAgent样式

层叠:样式层叠,是 CSS 一个基本特征,它定义如何合并来自多个源的属性值的算法。某种意义上,它处于核心地位,具体的层叠规则属于深入 CSS 语言的范畴,这里就补展开篇幅说了。

不过值得注意的是,在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle当中,也就是可以通过 JS 来获取计算后的样式,非常方便。

这个阶段,完成了 DOM 节点中每个元素的具体样式,计算过程中要遵循 CSS 的继承层叠两条规则,最终输出的内容是每个节点 DOM 的样式,被保存在 ComputedStyle 中。

想了解每个 DOM 元素最终的计算样式,可以打开 Chrome 的“开发者工具”,选择第一个“element”标签,选择 div 标签,然后再选择“Computed”子标签

# (题外话)另一种说法 CSSOM

image-20211112103255390

跟处理 HTML 一样,我们需要更具 CSS 两个规则:继承层叠转换成某种浏览器能理解和处理的东西,处理过程类似处理 HTML,如上图 ☝

CSS 字节转换成字符,接着转换成令牌和节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树结构内

image-20211112103300000

很多人肯定看这个很熟悉,确实,很多博客都是基于 CSSOM 说法来讲的,我要说的是:

和 DOM 不一样,源码中并没有 CSSOM 这个词,所以很多文章说的 CSSOM 应该就是 styleSheets,当然了这个 styleSheets 我们可以打印出来的

很多文章说法是渲染树也是 16 年前的说法,现在代码重构了,我们可以把 LayoutTree 看成是渲染树,不过它们之间还是有些区别的。

# 生成布局树

上述过程已经完成 DOM 树(DOM 树)构建,以及样式计算(DOM 样式),接下来就是要通过浏览器的布局系统确定元素位置,也就是生成一颗布局树(Layout Tree),之前说法叫 渲染树

# 创建布局树

  1. 在 DOM 树上不可见的元素,head 元素,meta 元素等,以及使用 display:none 属性的元素,最后都不会出现在布局树上,所以浏览器布局系统需要额外去构建一棵只包含可见元素布局树

  2. 我们直接结合图来看看这个布局树构建过程:

    image-20211112103309669

  3. 为了构建布局树,浏览器布局系统大体上完成了下面这些工作:

  • 遍历 DOM 树可见节点,并把这些节点加到布局树中
  • 对于不可见的节点,head,meta 标签等都会被忽略。对于 body.p.span 这个元素,它的属性包含 display:none,所以这个元素没有被包含进布局树。

# 布局计算

接下来就是要计算布局树节点的坐标位置,布局的计算过程非常复杂,张开介绍的话,会显得文章过于臃肿,大多数情况下,我们只需要知道它所做的工作是什么,想知道它是如何做的话,可以看看以下两篇文章 👇

人人 FED 团队的文章-从 Chrome 源码看浏览器如何 layout 布局 (opens new window)

从 Chrome 源码看浏览器如何 layout 布局 (opens new window)

# 梳理前三个阶段

image-20211112103316625

# 分层

  • 生成图层树(Layer Tree)
  • 拥有层叠上下文属性的元素会被提升为单独一层
  • 需要裁剪(clip)的地方也会创建图层
  • 图层绘制

首先需要知道的就是,浏览器在构建完布局树后,还需要进行一系列操作,这样子可能考虑到一些复杂的场景,比如一些些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,还有比如是含有层叠上下文如何控制显示和隐藏等情况。

# 生成图层树

你最终看到的页面,就是由这些图层一起叠加构成的,它们按照一定的顺序叠加在一起,就形成了最终的页面。

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

我们来看看图层与布局树之间关系,如下图 👇

image-20211112103324645

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

那什么情况下,渲染引擎会为特定的节点创建新图层呢?

有两种情况需要分别讨论,一种是显式合成,一种是隐式合成

# 显式合成

# 一、 拥有层叠上下文的节点。

层叠上下文也基本上是有一些特定的 CSS 属性创建的,一般有以下情况:

  1. HTML根元素CanvasVideoDocument元素本身就具有层叠上下文,

  2. 普通元素设置position 不为 static并且设置了 z-index 属性,会产生层叠上下文。

  3. 元素的 transform 值不是 none

  4. 元素的 isolation 值是 isolate

  5. **will-change**指定的属性值为上面任意一个。(will-change 的作用后面会详细介绍)

  6. 隐藏背面(backface-visibility: hidden

  7. 倒影(box-reflect

  8. column-count(不为 auto)或者column-widthZ(不为 auto)

  9. 透明的(opacity 小于 1)、滤镜(filter)、遮罩(mask)、混合模式(mix-blend-mode 不为 normal

  10. 对不透明度(opacity)、变换(transform)、滤镜(filter)应用动画

  11. OverflowClipLayer

  12. 剪切溢出内容(overflow: hidden

  13. 剪切路径(clip-path

# 二、需要剪裁(clip)的地方。

比如一个标签很小,50*50 像素,你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条也会被单独提升为一个图层,如下图

image-20211112103337940

数字 1 箭头指向的地方,可以看看,可能效果不是很明显,大家可以自己打开这个 Layers 探索下。

元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。

# 隐式合成

这是一种什么样的情况呢,通俗意义上来说,就是z-index比较低的节点会提升为一个单独的途图层,那么层叠等级比它高的节点**都会**成为一个独立的图层。

浏览器渲染流程&Composite(渲染层合并)简单总结 (opens new window)

缺点: 根据上面的文章来说,在一个大型的项目中,一个z-index比较低的节点被提升为单独图层后,层叠在它上面的元素统统都会提升为单独的图层,我们知道,上千个图层,会增大内存的压力,有时候会让页面崩溃。这就是层爆炸

# 绘制

完成了图层的构建,接下来要做的工作就是图层的绘制了。图层的绘制跟我们日常的绘制一样,每次都会把一个复杂的图层拆分为很小的**绘制指令,然后再按照这些指令的顺序组成一个绘制列表**,类似于下图 👇

image-20211112103343436

从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。

大家可以在 Chrome 开发者工具中在设置栏中展开 more tools, 然后选择Layers面板,就能看到下面的绘制列表:

image-20211112103405779

在该图中,**箭头 2 指向的区域 **就是 document 的绘制列表,**箭头 3 指向的拖动区域 **中的进度条可以重现列表的绘制过程。

当然了,绘制图层的操作在渲染进程中有着专门的线程,这个线程叫做合成线程

# 分块

  • 接下来我们就要开始绘制操作了,实际上在渲染进程中绘制操作是由专门的线程来完成的,这个线程叫合成线程
  • 绘制列表准备好了之后,渲染进程的主线程会给合成线程发送commit消息,把绘制列表提交给合成线程。接下来就是合成线程一展宏图的时候啦。

你想呀,有时候,你的图层很大,或者说你的页面需要使用滚动条,然后页面的内容太多,多的无法想象,这个时候需要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

  • 基于上面的原因,合成线程会讲图层划分为图块(tile)
  • 这些块的大小一般不会特别大,通常是 256 _ 256 或者 512 _ 512 这个规格。这样可以大大加速页面的首屏展示

首屏渲染加速可以这么理解:

因为后面图块(非视口内的图块)数据要进入 GPU 内存,考虑到浏览器内存上传到 GPU 内存的操作比较慢,即使是绘制一部分图块,也可能会耗费大量时间。针对这个问题,Chrome 采用了一个策略: 在首次合成图块时只采用一个**低分辨率**的图片,这样首屏展示的时候只是展示出低分辨率的图片,这个时候继续进行合成操作,当正常的图块内容绘制完毕后,会将当前低分辨率的图块内容替换。这也是 Chrome 底层优化首屏加载速度的一个手段。

# 光栅化

接着上面的步骤,有了图块之后,合成线程会按照视口附近的图块优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图

  • 图块是栅格化执行的最小单位
  • 渲染进程中专门维护了一个**栅格化线程池,专门负责把图块转换为位图数据**
  • 合成线程会选择视口附近的图块(tile),把它交给栅格化线程池生成位图
  • 生成位图的过程实际上都会使用 GPU 进行加速,生成的位图最后发送给合成线程

运行方式如下 👇

image-20211112103414603

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫**快速栅格化**,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

image-20211112103420641

从图中可以看出,渲染进程把生成位图的指令发送给 GPU,然后在 GPU 中执行指令,生成图块的位图,并保存在 GPU 的内存中。

# 合成和显示

栅格化操作完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。

浏览器进程中的viz组件接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡,那你肯定对显卡的原理很好奇。

看了某博主对显示器显示图像的原理解释:

无论是 PC 显示器还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区后缓冲区对换位置,如此循环更新。

这个时候,心中就有点概念了,比如某个动画大量占用内存时,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象


用一张图来总结 👇

image-20211112103429579

我们把上面整个的渲染流水线,用一张图片更直观的表示 👇

image-20211112103434423

# 回流-重绘-合成

更新视图三种方式

  • 回流
  • 重绘
  • 合成

# 重排(回流/布局)

Render Object 在添加到树之后,还需要重新计算位置和大小;ComputedStyle 里面已经包含了这些信息,为什么还需要重新计算呢?因为像 margin: 0 auto; 这样的声明是不能直接使用的,需要转化成实际的大小,才能通过**绘图引擎**绘制节点;这也是 DOM TreeCSSOM Tree 需要组合成 Render Object Tree 的原因之一。

布局是从 Root Render Object 开始递归的,每一个 Render Object 都有对自身进行布局的方法。

为什么需要递归(也就是先计算子节点再回头计算父节点)计算位置和大小呢?

因为有些布局信息需要子节点先计算,之后才能通过子节点的布局信息计算出父节点的位置和大小;

例如父节点的高度需要子节点撑起。如果子节点的宽度是父节点高度的 50%,要怎么办呢?这就需要在计算子节点之前,先计算自身的布局信息,再传递给子节点,子节点根据这些信息计算好之后就会告诉父节点是否需要重新计算。

重排触发的条件就是:对 DOM 结构的修改引发 DOM 几何尺寸变化的时候,会发生回流过程。

具体一点,有以下的操作会触发回流:

  1. 一个 DOM 元素的几何属性变化,常见的几何属性有widthheightpaddingmarginlefttopborder 等等, 这个很好理解。
  2. 使 DOM 节点发生增减或者移动
  3. 读写 offset族、scroll族和client族属性的时候,浏览器为了获取这些值,需要进行回流操作。
  4. 调用 window.getComputedStyle 方法。

一些常用且会导致回流的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

依照上面的渲染流水线,触发回流的时候,如果 DOM 结构发生改变,则重新渲染 DOM 树,然后将后面的流程(包括主线程之外的任务)全部走一遍。

image-20211112103440561

# 重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

根据概念,我们知道由于没有导致 DOM 几何属性的变化,因此元素的位置信息不需要更新,从而省去布局的过程,流程如下:

image-20211112103444788

跳过了**布局树**和建图层树,直接去绘制列表,然后在去分块,生成位图等一系列操作。

可以看到,重绘不一定导致回流,但回流一定发生了重绘

# flush 队列

部分浏览器缓存了一个flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来,或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队列,但是当我们访问一些及时属性时,浏览器会为了获得此时此刻、最准确的属性值,而提前将 flush 队列的任务出队列

# 合成

还有一种情况:就是更改了一个既不要布局也不要绘制的属性,那么渲染引擎会跳过布局和绘制,直接执行后续的合成操作,这个过程就叫合成

举个例子:比如使用CSStransform来实现动画效果,避免了回流跟重绘直接在非主线程中执行合成动画操作。显然这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

利用这一点好处:

  • 合成层的位图,会交由 GPU(显卡) 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transformopacity 效果,不会触发 layoutpaint

提升合成层的最好方式是使用 CSSwill-change 属性

# GPU 加速原因

比如利用 CSS3 的transformopacityfilter这些属性就可以实现合成的效果,也就是大家常说的**GPU 加速**。

  • 在合成的情况下,直接跳过布局和绘制流程,进入非主线程处理部分,即直接交给合成线程处理
  • 充分发挥GPU优势,合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,而GPU 是擅长处理位图数据的。
  • 没有占用主线程的资源,即使主线程卡住了,效果依然流畅展示。

# 实践意义

  • 使用createDocumentFragment进行批量的 DOM 操作
  • 对于 resizescroll 等进行防抖/节流处理。
  • 动画使用transform或者opacity实现
  • 将元素的will-change 设置为 opacity、transform、top、left、bottom、right 。这样子渲染引擎会为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。
  • 对于不支持will-change 属性的浏览器,使用一个3D transform属性来强制提升为合成 transform: translateZ(0);
  • requestAnimationFrame优化等等

# 面试知识点

# Load 和 DOMContentLoaded 区别

Load 事件触发代表页面中的 DOMCSSJS,图片已经全部加载完毕。

DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSSJS,图片加载。

# 图层(Layer)

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用(层爆炸)。

通过以下几个常用属性可以生成新图层

  • 3D 变换:translate3dtranslateZ
  • will-change
  • videoiframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

# 重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。

所以以下几个动作可能会导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

很多人不知道的是,重绘和回流其实和 Event loop 有关。

  1. Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

以上内容来自于 HTML 文档 (opens new window)

# 减少重绘和回流

  • 使用 translate 替代 top

    <div class="test"></div>
    <style>
      .test {
        position: absolute;
        top: 10px;
        width: 100px;
        height: 100px;
        background: red;
      }
    </style>
    <script>
      setTimeout(() => {
        // 引起回流
        document.querySelector('.test').style.top = '100px'
      }, 1000)
    </script>
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    for (let i = 0; i < 1000; i++) {
      // 获取 offsetTop 会导致回流,因为需要去获取正确的值
      console.log(document.querySelector('.test').style.offsetTop)
    }
    复制代码
    
    1
    2
    3
    4
    5
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深

  • 频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

# 行内样式和 CSSStyleSheet 的区别

每个 link引入的CSS 文件嵌入样式(style 标签)都会对应一个 CSS StyleSheet 对象

这个对象由一系列的 Rule(规则) 组成;每一条 Rule 都会包含 Selectors(选择器) 和若干 Declearation(声明),Declearation 又由 Property(属性)和 Value(值)组成。

另外,浏览器默认样式表(defaultStyleSheet)和用户样式表(UserStyleSheet)也会有对应的 CSSStyleSheet 对象,因为它们都是单独的 CSS 文件。

至于行内样式,在构建 DOM Tree 的时候会直接解析成 Declearation 集合挂载到元素的 style 属性上。

所有的 CSS StyleSheet 都挂载在 document 节点上,我们可以在浏览器中通过 document.styleSheets 获取到这个集合。行内样式可以直接通过节点的 style 属性查看。

通过一个例子,来了解下行内样式和 CSSStyleSheet 的区别:

--------demo.html---------
<!DOCTYPE html>
<html lang="en">
  <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" />
    <style>
      body .div1 {
        line-height: 1em;
      }
    </style>
    <link rel="stylesheet" href="./style.css" />
    <style>
      .div1 {
        background-color: #f0f;
        height: 20px;
      }
    </style>
    <title>Document</title>
  </head>

  <body>
    <div class="div1" style="font-size: 12px;">test</div>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
----------style.css--------
@import "./index.css";
.div1{
    background-color: #fff;
}
1
2
3
4
5
----------index.css---------- .div1 {
  height: 80px;
}
1
2
3

可以看到一共有三个 CSSStyleSheet 对象,每个 CSSStyleSheet 对象的 rules 里面会有一个 style 属性,style 属性的值就是 CSSStyleDeclaration,而行内样式获取到的直接就是 CSSStyleDeclaration

(在一个css文件中通过@import引入另一个css文件,会合并成一个CSS StyleSheet

image-20201210174928221

image-20201210174949382

# 参考

「浏览器工作原理」写给女友的秘籍-渲染流程篇(1.1W 字) (opens new window)

网页渲染性能优化 —— 渲染原理 (opens new window)

最近更新: 4 小时前