浏览器 - 页面渲染流程

48 阅读24分钟

引: 在浏览器地址栏输入URL到页面展示,这中间的大致过程为:

  1. 用户输入URL并回车;
    • 1.1 如果没有监听onbeforeunload事件,进入流程2
    • 1.2 如果监听onbeforeunlaod事件,则执行其内部逻辑,若用户同意此次导航,进入流程2,若拒绝直接返回。
  2. 浏览器进程解析输入内容:
    • 2.1 如果识别输入的是一个URL,就尝试请求该URL
    • 2.2 否则视为搜索关键字,浏览器尝试将关键字发送给默认的搜索引擎(合成新的URL并请求)
  3. URL请求过程,浏览器进程通过进程间通信(IPC)把URL请求发送给网络进程网络进程接收到URL请求后,检查本地缓存是否命中该请求资源:
    • 3.1 如果命中,则将该缓存资源直接返回给浏览器进程,进入流程6
    • 3.2 如果没有命中,进入流程4
  4. 网络进程向web服务器发起HTTP请求,请求流程如下:
    • 4.1 进行DNS解析,获取服务器IP地址和端口
    • 4.2 利用IP地址和服务器建立TCP连接
    • 4.3 构建请求行、请求头(请求体)信息,附加Cookie
    • 4.4 发送请求信息,服务器根据浏览器的请求信息准备相应的内容(响应行、响应头和响应体)发送给网络进程
  5. 网络进程接收并解析响应数据
    • 5.1 服务器返回响应行(协议版本和状态码)
    • 5.2 检查状态码,如果是301/302(301永久重定向:将原始请求的缓存标记为永久失效,立即更新缓存,再次遇到此请求,直接打到重定向之后的URL,不会再经过重定向流程,302临时重定向:将原始请求的缓存标记为临时失效,浏览器会更新缓存,但仍保留原始请求的缓存标识),则需要重定向,从Location字段中读取地址,重新进行流程3,如果是200,则继续处理请求。
    • 5.3 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行 后续的渲染,如果是text/html,则通知浏览器进程 准备渲染进程(准备进行渲染)。
  6. 准备渲染进程
    • 6.1 网络进程读取响应头数据,将其转发给浏览器进程,浏览器接收到网络进程的响应头数据后,携带响应头等基本信息发送CommitNavigation到渲染进程,让其准备接收数据(此时文档数据还在网络进程中);
  7. 提交文档阶段
    • 7.1 浏览器进程将网络进程接收到的响应头数据提交给渲染进程(提交文档消息),渲染进程收到消息后和网络进程建立传输数据的“管道”
    • 7.2 渲染进程接收完数据后,向浏览器进程返回“确认提交”消息
    • 7.3 浏览器进程接收到“确认提交”消息后,更新浏览器界面状态(包含安全状态、地址栏URL、前进后退的历史状态)并更新web页面。
  8. 渲染阶段
    • 解析HTML、CSS、Javascript数据,和子资源加载,将页面数据交给浏览器进程,完成页面显示。

截至渲染阶段,相当于浏览器已经获取到了HTML数据,渲染阶段就是把这些资源数据变成页面的过程。

Screenshot 2024-03-27 at 21.58.30.png

在此之前,先简单了解HTML、CSS和JS的含义:

Screenshot 2024-03-27 at 22.00.17.png

HTML(HyperText Markup Language)的内容由标记和文本组成。标记也称为标签,每个标签都有自己的语义,浏览器会根据标签的语义来正确展示HTML内容。(比如上面<p>标签是告诉浏览器在这里的内容需要创建一个新段落,中间的文本信息就是段落中需要显示的内容)

如果需要改变HTML的文本颜色、大小等信息,就需要用到CSS。CSS(Cascading Style Sheets)是层叠样式表,是由选择器和属性组成。(如图中的p选择器,它会把HTML里面<p>标签的内容选择出来,然后再把选择器的属性值应用到<p>标签内容上。选择器里面有个color属性,它的值是red,就是告诉渲染引擎把<p>标签内容显示为红色)

至于 JS(Javascript),可以使用它让网页的内容产生变化。(如上图,可以通过JS来修改CSS样式值,从而让文本颜色变为灰色)

渲染流程是一个十分复杂的过程,在执行阶段会被划分为很多子阶段,输入的HTML等资源经过这些子阶段的处理,最终输出位图,用于页面的成像。整个这一处理流程可以称为渲染流水线

Screenshot 2024-03-27 at 22.13.41.png

渲染流水线可以分为如下子阶段: 构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成

每个子阶段:

  • 在开始,都有其输入内容
  • 然后有其处理过程
  • 最终,产生输出内容

1. 构建DOM树

浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构--DOM树。顾名思义,它是一个树形结构,其构建过程大致如下图:

Screenshot 2024-03-27 at 22.21.12.png

输入内容 为 HTML文件;
经由 HTML 解析器解析;
最终,输出内容为树形结构的DOM。

可以通过开发者工具,在控制台输入“document”来直观的感受DOM树结构。

Screenshot 2024-03-27 at 22.26.50.png

DOM 和 HTML 内容几乎是一样的,不同的是,DOM是保存在内存中的树状结构。可以通过Javascript 来查询或修改其内容。

document.getElementsByTagName("p")[0].innerText = "black"

这行代码的作用就是把第一个<p>标签的内容修改为black,执行完毕后,界面如下图所示:

Screenshot 2024-03-27 at 22.29.48.png

从图中可见,在执行了JS代码后, DOM的第一个p节点的内容被修改,同时页面内容有被修改。

2. 样式计算

有了DOM树, 但是DOM节点的样式并不清楚,要让DOM节点拥有正确的样式, 就需要样式计算。

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大致可分为三步。

2.1 把 CSS 转换为浏览器能够理解的结构

CSS样式的来源主要有三种:

  • 通过link引用的外部CSS文件;
  • <style> 标签内的CSS;
  • 元素的 style 属性内嵌的CSS;

Screenshot 2024-03-27 at 22.36.39.png

和HTML文件一样,浏览器无法直接理解这些纯文本的CSS样式, 当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构 -- styleSheets

相应地,可以在控制台输入“document.styleSheets”查看其结构。

Screenshot 2024-03-27 at 22.44.25.png

样式表包含了很多样式,已经把三种来源的样式都包含进去了。且该结构同时具备查询和修改样式功能。

2.2 转换样式表中的属性值,使其标准化

至此,从结构上,浏览器可以理解styleSheets,但是一些属性值为了让开发者易读而不被浏览器理解。

body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }

如上面代码中的 2em、blue、bold。需要将这些值转换为渲染引擎容易理解的值, 这个过程就是属性值的标准化

Screenshot 2024-03-27 at 22.52.25.png

最终,2em被解析成32px, red被解析成rgb(255,0,0),bold被解析成700...

2.3 计算出DOM树中每个节点的具体样式

DOM节点的样式是根据CSS的继承规则层叠规则来计算的。

2.3.1 CSS 样式继承

CSS 继承就是每个DOM节点都包含有父节点的样式。

body { font-size: 20px }
p {color:blue;}
span  {display: none}
div {font-weight: bold;color:red}
div  p {color:green;}

这张样式表最终应用到DOM节点的效果如图:

Screenshot 2024-03-27 at 22.58.17.png

可以看出, 所有子节点都继承了父节点样式,比如body节点的font-size属性是20,body节点下的所有子节点的font-size都是20。这就是样式计算的继承特性。

可以通过Chrome开发者工具,查看 Elements 标签的 Styles 子标签

Screenshot 2024-03-27 at 23.04.16.png

  • 可以通过点击区域1中的任意元素,来查看其元素的样式(区域2),如选择的元素是<p>标签,位于html.body.div这个路径下;
  • 可以从样式来源(区域3)中查看样式的具体来源(源于样式文件,还是UserAgent样式表)。其中UserAgent样式是浏览器提供的一组默认样式,如果未提供任何样式,则默认使用的就是UserAgent样式
  • 结合区域2和区域3 可以查看样式继承的具体过程。

2.3.2 CSS 样式层叠

层叠规则是CSS基本特性,在CSS中处于核心地位(CSS全称“层叠样式表”正是强调了这一点)。它指定了在应用多个样式定义到同一个元素时,如何确定最终的样式结果。这些规则决定了样式的优先级和应用顺序。

层叠的规则如下:

  • 选择器特异性(Specificity): 当多个选择器都应用到同一个元素时,选择器特异性用于确定哪个样式具有更高的优先级。特异性通过计算选择器中的各个部分的权重来决定,例如类选择器的特异性低于id选择器
  • 内联样式(inline Styles): 是通过在HTML元素的style属性中直接定义样式。内联样式具有最高优先级,会覆盖其他类型的样式定义。
  • 内部样式表(internal styleSheets): 是在HTML文档的<head>标签内使用<style>标签定义的样式表。内部样式表的优先级高于外部样式表。
  • 外部样式表(external styleSheets): 是通过<link>标签或@import规则引入的外部CSS文件。外部样式表的优先级低于内联样式和内部样式表。
  • 样式规则顺序:当存在相同特异性的样式规则时,后面的规则会覆盖前面的规则。
  • !important:通过在样式规则后添加 !important 声明,可以将该规则的优先级提升到最高。但是,滥用 !important 可能会导致样式难以维护和调试,应谨慎使用!

样式计算阶段的目的是为了计算出DOM节点中每个元素的具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。这个阶段 最终输出的内容是每个DOM节点的样式,并被保存在ComputedStyle的结构内。

可以通过Chrome开发者工具,查看 Elements 标签的 Computed 子标签来查看最终的计算样式。

Screenshot 2024-03-27 at 23.18.21.png

问: 如果下载 CSS 文件阻塞了,会阻塞 DOM 树的合成吗?会阻塞页面的显示吗?

阻塞DOM树的合成
当从服务器接收HTML页面的第一批数据时,DOM解析器就开始工作了,在解析过程中,如果遇到了JS脚本,那么DOM解析器会先执行JavaScript脚本,执行完成之后,再继续往下解析。如果这中间遇到需要下载的js或css文件,则先要下载,再执行,再解析。
阻塞页面的显示
在现代浏览器中,通常有一个默认的样式表(User Agent stylesheet),用于为那些还没有加载或解析CSS的元素提供基本的样式。这意味着即使CSS文件被阻塞,页面上的元素也不会完全没有样式,而是使用浏览器提供的默认样式显示。
优化建议

  • 优化CSS文件:压缩CSS文件大小,减少HTTP请求,例如通过合并多个CSS文件为一个。
  • 使用媒体查询:通过媒体查询加载非关键CSS,例如打印样式或者大屏幕设备的样式,从而确保基础样式的快速加载。
  • 异步加载CSS:使用<link rel="stylesheet" async>标签来异步加载CSS文件,这样即使CSS文件下载被阻塞,也不会影响HTML的解析和DOM树的构建。
  • 关键渲染路径优化:识别并优先加载对首屏渲染最重要的CSS资源,以最小化页面渲染的延迟。
  • 使用相对路径:尽量使用相对路径而不是绝对路径来引用CSS文件,这样可以减少DNS查询的时间。

然而,如果CSS文件的下载被长时间阻塞,页面的最终样式将无法应用,这会导致页面在加载过程中显示为默认样式,直到CSS文件被成功加载和解析。这种情况下,用户可能会看到一个“闪烁”的效果,即页面从默认样式突然转变为最终样式。

3. 布局阶段

经过前两个阶段,DOM树以及DOM树中元素的样式已经有了,接下来是确定DOM树中可见元素的几何位置,这个过程叫做布局

3.1 创建布局树

DOM树中含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。在显示之前,我们还需要额外地构建一棵只包含可见元素的布局树。

Screenshot 2024-03-27 at 23.23.32.png

DOM树中所有不可见的节点都没有包含在布局树中。(布局树是DOM树的子集)

为了构建布局树,浏览器大致完成了这些工作:

  • 遍历DOM树中的所有可见节点,并把这些节点加到布局树中;
  • 不可见的节点会被布局树忽略掉

3.2 布局计算

获取到完整的布局树后,就要计算布局树各节点的坐标位置和大小了。浏览器会从布局树的顶部开始,递归地计算每个元素的布局信息

  • 对于块级元素,浏览器会根据元素的widthheightmarginpaddingborder等属性计算其大小和位置。
  • 对于行内元素,浏览器会根据元素的字体大小、字体宽度和其他相关样式属性来确定它们的大小,并按照文档流进行排列。

Chrome在老的布局框架中,Blink使用一种称为“可变树”的布局系统,在这个系统里,输入和输出并不分离。

新的布局框架LayoutNG中引入了不可变的 “片段树”,明确的区分出了输入和输出结果,从而允许在增量布局时重用布局树中的大部分节点

4. 分层

有了布局树,每个元素的具体位置信息也计算出来了,是否可以直接开始着手绘制页面了?

答案是否定的。

因为页面中有很多复杂的效果,如一些复杂的3D变换、页面滚动或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果(或者说以最高性能标准去实现这些效果),渲染引擎还需要为特定的节点生成专用的图层,并生成对应的图层树(LayerTree)。

可以通过Chrome开发者工具,选择“Layers”标签,就可以可视化页面的分层情况。

Screenshot 2024-03-28 at 23.44.46.png

渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。

这些图层与布局树节点之间的关系如下:

Screenshot 2024-03-28 at 23.46.22.png

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

那么满足什么条件,渲染引擎才会为特定节点创建新的图层呢?

  • 第一点,拥有层叠上下文属性的元素会被提升为单独的一层。

    页面是个二维平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。 Screenshot 2024-03-28 at 23.50.47.png 如拥有position属性、z-index属性、filter属性、opacity不为1的元素等,可能会被创建为单独图层(以便它们可以独立于其他元素进行渲染和合成)。

  • 第二点,需要裁剪的地方也会被创建为图层。

    当我们限定div大小,而div内的文字内容又比较多时,文字所显示的区域超出限定大小,这时就会产生裁剪,渲染引擎会把裁剪文字内容的一部分用于显示在div区域。为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。

Screenshot 2024-03-28 at 23.57.09.png

  • 其他使用硬件加速的属性(transform、perspective)、关键帧动画、video元素、网页的root节点 这些也可能会创建更多的单独的图层。

5. 图层绘制

完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。绘制的过程遵循画家算法。即由远及近,从离屏幕最远的图层开始,依次绘制。

渲染引擎将图层的绘制拆分成很多小的绘制指令, 然后再把这些指令按照顺序组成一个待绘制列表。

Screenshot 2024-03-29 at 00.02.10.png

绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表

可以通过Chrome开发者工具的 Layers 标签,选择 document层,来实际感受绘制列表。

Screenshot 2024-03-29 at 09.06.27.png

区域1就是 document 的绘制列表, 拖动区域2中的进度条可以重现列表的绘制过程。

6.栅格化(raster)

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上的绘制操作是由渲染引擎中的合成线程来完成的。下图揭示了渲染引擎主线程和合成线程之间的关系。

Screenshot 2024-03-29 at 09.09.55.png

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。

那么合成线程如何工作呢?

通常一个页面可能很大,但是用户只能看到其中的一部分,剩余部分需要滚动才能看到,用户能看到的区域称为 视口(viewport)

Screenshot 2024-03-29 at 09.14.18.png

如果页面需要滚动很久才能到底部,要绘制出所有图层内容的话,开销会很大,也没有必要,所以通常,合成线程会将图层划分为图块(tile),然后优先将视口附近的图块生成位图。

Screenshot 2024-03-29 at 09.19.16.png

实际生成位图的操作是由栅格化来执行的,所谓栅格化,就是指将图块转换为位图的过程。图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有图块栅格化都是在线程池内执行的。

Screenshot 2024-03-29 at 13.20.01.png

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

GPU操作是运行在GPU进程中的,这其中还涉及跨进程操作。

Screenshot 2024-03-29 at 13.27.14.png

如图, 渲染进程把生成图块的指令发给GPU, GPU中执行生成图块的位图,并保存在GPU的内存中。

7. 合成和显示

一旦所有的图块都被光栅化,合成线程就会生成一个绘制图块的命令-“DrawQuad”,然后将该命令提交给浏览器进程

图块光栅化过程通常是逐步进行的,而不是等待所有图块全部完成才提交渲染。

在合成过程中,每个图层都会被光栅化,浏览器会优先光栅化视口(viewport)附近的图块,这样可以更快地将内容显示给用户。

现代浏览器通常采用异步渲染策略,这意味着浏览器会根据优先级和资源可用性来决定光栅化的顺序。如果视口附近的图块已经光栅化并准备就绪,浏览器可以立即将它们渲染到屏幕上,而不需要等待整个页面的所有图块都完成。当页面上的元素发生变化,或者用户与页面交互时(如滚动、缩放等),浏览器会更新布局树和图层,并重新进行光栅化和合成,以反映这些变化。

浏览器进程中有一个叫viz(Visuals)的组件,用来接收合成线程发送过来的DrawQuad命令,然后将其页面内容绘制到内存(显存)中,最后将其显示在屏幕上。

在每一帧的工作周期中,浏览器会根据当前的图层状态和任何新的变化来更新图层,然后将其合成到一起。这个过程是异步的,并且通常会在浏览器的主渲染线程之外进行,以避免阻塞页面的交互。这包括:

  • 帧同步
    • 浏览器会根据显示器的刷新率(通常是60Hz)来同步图层的更新。
    • 这意味着浏览器会尝试每16.67毫秒(大约)渲染一帧,以匹配显示器的刷新周期。
  • 帧开始
    • 在每一帧的开始,浏览器会检查是否有待处理的动画、滚动或其他需要更新的操作。
    • 如果有,浏览器会触发相应的JavaScript事件(如requestAnimationFrame)来更新页面内容。
  • 帧更新
    • 浏览器会根据需要更新图层的内容,这可能包括重新计算布局、应用样式更改、处理用户交互等。
    • 更新操作会触发Layer Compositor 对图层进行重新绘制和光栅化。
  • 帧提交
    • 一旦图层更新完成,浏览器会将它们提交给Layer Compositor 进行合成。
    • Layer Compositor 会将所有更新的图层合并成最终的页面视图。
  • 帧显示
    • 合成后的页面视图会被发送到显示器,用户看到的是更新后的页面。
    • 如果浏览器能够稳定地保持每16毫秒渲染一帧,用户会感受到流畅的动画和交互。

至此,从Web服务器上获取到的HTML,CSS,Javascript等文件,经过整个渲染流水线的处理,最终显示出完整页面。

Screenshot 2024-03-29 at 13.34.34.png

一个完整的渲染流程大致可总结为如下:

    1. 渲染进程将HTML内容转换为浏览器能够识别的 DOM树 结构;
    1. 渲染引擎将CSS样式表转换为浏览器可以理解的styleSheets,并计算出DOM节点的样式;
    1. 创建布局树,并计算元素的布局信息;
    1. 对布局树进行分层,生成图层树
    1. 为每个图层生成绘制列表,并提其提交到合成线程;
    1. 合成线程将图层分为图块,并在光栅化线程池中将图块转换成位图
    1. 合成线程发送绘制图块命令DrawQuad给浏览器进程;
    1. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

重排、重绘和合成

1. 重排 - 更新了元素的几何属性

Screenshot 2024-03-29 at 13.36.55.png

如果通过 JS 或者 CSS 修改元素的几何位置属性,如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析从布局阶段开始的后续一系列子阶段,此过程就叫做重排。重排需要更新完整的渲染流水线,所以开销很大

2. 重绘 - 更新元素的绘制属性

Screenshot 2024-03-29 at 13.37.10.png

如果通过JS更改某些元素的背景颜色,布局阶段并不会执行,但是会触发渲染流水线绘制及之后的一系列阶段,此过程就叫做重绘。相较于重排,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些

3. 直接合成阶段

Screenshot 2024-03-29 at 13.37.23.png

如果更改一个既不要布局也不要绘制的属性,如使用CSS的 transform 来实现动画效果,渲染引擎将跳过布局和绘制,只执行后续的合成操作,这个过程就叫做合成

该过程直接在非主线程上执行动画操作,并没有占用主线程资源,所以,相对于重排和重绘,合成性能效率大大提升。

4. 如何减少重排和重绘

重排(Reflow)和重绘(Repaint)是Web页面性能优化中的两个重要概念,这两个过程都是资源密集型的,可能导致页面卡顿或性能下降,减少重排和重绘,相当于减少了渲染进程的主线程和非主线程的很多计算和操作,对于性能要求较高的页面, 需要适当规避重排和重绘。

  1. 避免频繁的样式变更
    频繁改变元素的样式属性(尤其是影响布局的属性,如widthheightmarginpadding等)会导致浏览器不断进行重排。尽量减少这些属性的更改次数,或者将它们合并到一次操作中(如:1)使用class来集中修改样式,而不是通过style一个个的修改;2)对DOM属性的读写要分离)。
    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次重排+重绘 
    
    修改为读写分离操作,触发一次重排 
    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);
    
  2. 慎用table布局
    1) 由于浏览器使用流式布局,对 Render Tree 的计算通常只需要遍历一次就可以完成,但 Table 及内部元素除外,通常需要多次计算且需花费3倍同等元素时间。
    2) 其次,很小的改动可能会导致整个 table 都重新布局。
  3. 批量DOM操作
    例如 createDocumentFragment,或者使用框架,例如 React:批量操作 DOM 可以减少重排重绘的次数。
  4. Debounce window resize 事件
    防抖可以防止频繁触发重排重绘,例如 window resize 可以设置为 1s 内只可以触发一次。
  5. 使用transform代替topleft
    使用transform属性进行元素的移动和定位可以避免触发重排,因为transform通常只会引起重绘。
  6. 优化JavaScript动画
    在动画中使用requestAnimationFrame来确保动画在浏览器的绘制周期中运行,这样可以减少不必要的重绘。
  7. 使用will-change属性
    will-change属性可以告诉浏览器哪些元素可能会发生变化,浏览器可以预先准备这些变化,减少实际变化时的重排和重绘。
  8. 合理使用display: nonevisibility: hidden
    当需要隐藏元素时,使用display: none可以避免元素占据布局空间,从而减少重排。而visibility: hidden则会使元素不可见但仍占据空间。根据具体情况选择最合适的方法。
  9. 避免使用绝对定位
    绝对定位的元素可能会导致重排,因为它们的位置是相对于最近的已定位祖先元素。尽量使用相对定位或CSS Grid和Flexbox等现代布局技术。
  10. 使用CSS3属性
    CSS3提供了许多新的属性,如flexboxgrid,它们提供了更高效的方式来进行布局,可以减少重排的发生。
  11. 优化DOM结构
    减少DOM元素的数量和深度可以降低重排的复杂性。合并DOM节点、使用更少的嵌套和优化HTML结构都有助于提高性能。
  12. 使用开发者工具进行性能分析
    使用浏览器的开发者工具(如Chrome的Performance面板)可以帮助你识别哪些操作导致了重排和重绘,从而进行针对性的优化。