前端必知!-浏览器是如何渲染页面的?

1,023 阅读14分钟

前言

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环的作用下,渲染主线程会取出消息队列的渲染任务,开启渲染流程。(不知道事件循环的同学可以去我的这篇文章先了解一下事件循环 前端人必须理解的事件循环

image.png 整个渲染流程分为多个阶段,分别是:HTML解析,样式计算,布局,分层,绘制,分块,光栅化,画

每个阶段都有明确的输入输出,上一个阶段的输出会作为下一阶段的输入。

这样,整个渲染过程就形成了一套组织严密的生产流水线。 image.png

一、解析HTML

image.png 会生成两棵树,一棵是DOM树,另一棵是CSSOM树。

解析 HTML 生成 DOM

  1. 词法分析

    • 当浏览器接收到 HTML 文本时,首先进行词法分析。它将 HTML 文本分解成一个个的标记(tokens),例如<html><body><p>等标签以及它们包含的文本内容、属性等都被视为不同类型的标记。例如,对于 HTML 代码<p class="my - paragraph">Hello, world!</p>,词法分析器会识别出<p>开始标签标记、class="my - paragraph"属性标记、Hello, world!文本标记和</p>结束标签标记。
  2. 语法分析

    • 基于词法分析得到的标记,语法分析器会构建出 DOM 树。它根据 HTML 的语法规则将标记组合在一起。在构建 DOM 树的过程中,会识别出元素之间的嵌套关系。例如,在一个完整的 HTML 页面中,<html>标签是根节点,<body>标签是<html>的子节点,而<p>标签可能是<body>的子节点。对于以下简单 HTML:
     <html>
       <body>
         <h1>Title</h1>
         <p>Paragrap</p>
       </body>
     </html>
  • 会构建出一个以html为根的 DOM 树,其结构如下:

image.png

解析 CSS 生成 CSSOM

  1. 加载 CSS

    • 浏览器会通过多种方式加载 CSS,包括内联样式(在 HTML 元素的style属性中定义)、<style>标签内的样式和通过<link>标签引用的外部 CSS 文件。例如:

      • 内联样式:<p style="color: red;">This is a red paragraph.</p>
      • <style>标签样式:
       <style>
         h1 {
           font - size: 24px;
         }
       </style>
  • 外部 CSS 文件(通过<link>):<link rel="stylesheet" type="text/css" href="styles.css">
  1. 解析 CSS 规则

    • 对于加载的 CSS 内容,浏览器会进行解析。它将 CSS 样式规则解析成可以被理解和处理的格式。例如,对于 CSS 规则p { color: blue; margin - bottom: 10px; },浏览器会识别出选择器p以及对应的样式声明{ color: blue; margin - bottom: 10px; }
  2. 构建 CSSOM

    • 基于解析后的 CSS 规则,浏览器构建 CSSOM。CSSOM 是一个树形结构,其中每个节点代表一个 CSS 规则或样式声明。在 CSSOM 树中,样式会根据选择器的特异性(specificity)和层叠规则进行组织。例如,如果有以下 CSS 规则:
     body {
       font - family: Arial;
     }
     p {
       color: red;
     }
  • CSSOM 会将这些规则组织起来,并且当应用到 DOM 元素时,会根据特异性等规则确定最终应用的样式。如果一个<p>元素在<body>内,它会继承bodyfont - family样式,同时应用自身的color: red样式。
     body h1{
       font - size:3em;
       color:Red;
     }

下面是CSSOM树

image.png

Html解析过程中遇到CSS代码怎么办?

为了提高解析效率,浏览器会启动一个预解析器率先下载和解析CSS image.png

如果主线程解析到LINK位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后面的HTML。这是因为下载和解析CSS是在预解析线程上进行的,这就是CSS不会阻塞HTML解析的根本原因。

HTML解析过程中如果遇到JS代码怎么办?

渲染主线程遇到JS代码时必须暂停一切行为,等待下载执行完了之后才可以继续解析HTML,预解析线程可以帮忙分担一点下载JS的任务 image.png 如果主线程解析到Sctipt位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析完成后,才可以继续解析HTML,这是因为JS的执行过程可能会改变当前的DOM树,所以DOM树生成必须暂停。这就是JS会阻塞HTML的原因

为什么Js代码不会生成树?

因为JS代码只需要执行一遍就OK了,后续的步骤用不到JS,DOM树和cssom树会在后续的步骤中用到。

DOM 和 CSSOM 的关联

  • 一旦 DOM 和 CSSOM 都构建完成,浏览器会将两者关联起来。它会根据 CSSOM 中的样式规则,将样式应用到 DOM 中的相应元素上。这个过程会遍历 DOM 树中的每个元素,查找匹配的 CSSOM 规则,并计算出最终的样式。例如,当浏览器渲染一个<p>元素时,它会在 CSSOM 中查找适用于p元素的样式,然后将这些样式应用到 DOM 中的<p>元素节点,从而确定该元素在页面上的视觉呈现,如字体颜色、大小、背景等。

    DOM 和 CSSOM 的关联就是样式计算

二、样式计算

样式计算的结果是为每个元素确定最终的样式属性

首先会解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树,然后将两者合并生成渲染树(Render Tree)。在这个过程中,样式计算起着关键作用。它会处理 CSS 规则的优先级、继承关系等复杂情况,为每个元素确定最终的样式属性。

image.png 这一步完成之后,会得到一颗带样式的DOM树

三、布局

接下来是布局,布局之后会得到布局树。 渲染树和布局树的构建都依赖于 DOM 树和 CSSOM 树。渲染树主要是将 DOM 树中的可见元素与 CSSOM 树中的样式信息相结合,确定每个元素的显示样式。而布局树在渲染树的基础上,更侧重于计算每个元素的几何属性,如位置和大小。可以说,布局树是对渲染树的进一步细化处理,用于确定页面元素在屏幕上的具体布局。 布局完成后会得到布局树。 布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,比如节点的宽高、相对包含块的位置。 元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值(比如 position 被设置为 absolute 或 fixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。

Browser-Layout.png

大部分时候,DOM 树和 Layout 树并非一一对应的,为什么?

CSSLayoutTreeDisplay.png

比如某个节点的 display 设置为 none,这样的节点就没有几何信息,因此不会生成到 layout 树。有些使用了伪元素选择器,虽然 DOM 树中并不存在这些伪元素节点,但它们需要显示在浏览器上,所以会生成到 layout 树上。还有匿名行盒、匿名块盒等等都会导致 DOM 树和 layout 树无法一一对应。

CSSLayoutTreePseudoClass.png

image.png

div::before {

    content: '';

}
<div></div>

CSSPseudoClassLayout.png

四、分层 Layer

浏览器拿到布局后的 layout tree 会思考一些事情。大多数页面不是绘制后静止的,经常会有一些动画、用户点击后一些交互处理等,需要经常刷新页面。但如果整个页面直接重新刷新,这个性能开销是很大的。能不能提高效率?能,于是现在浏览器支持了分层。

主线程会使用一套复杂的策略对整个布局树进行分层。 分层的好处在于,将来在某一个层改变后,仅会对该层进行后续处理,从而提高效率。

Browser-Layer.png

滚动条、堆叠上下文有关的(z-index、transform、opacity )样式都会或多或少影响分层结果,也可以通过 will-change 属性更大程度的影响分层的结果。

html
 代码解读
复制代码
<div>
    100个<p>Lorem</p>
</div>

Browser-Layer-Demo.png

可以看到默认情况下浏览器对该代码分了2层。为什么滚动条需要单独设置一个 layer?因为100个 p 标签一定会超出一屏,所以会存在滚动条,且用户可能会高频滚动去查看内容,为了高效渲染,所以将滚动条单独设置了一个层。

假设我们已经通过技术手段得知某些内容经常会改变,但直接页面整体重刷会很耗费资源,那如何只对某一块区域设置一个 layer 呢?通过 will-change: transform 告诉浏览器,这块区域的 transform 属性经常会变。

will-change是一个用于通知浏览器某个元素即将发生变化的CSS属性。它可以被应用到任何元素上,用于提前告知浏览器该元素将要有哪些属性进行改变,从而优化渲染性能。

通过在元素上设置will-change属性,开发者可以明确指示浏览器对该元素进行优化处理。这样一来,浏览器可以提前分配资源和准备工作,以便在实际改变发生之前进行相应的合成操作。这样做有助于避免不必要的重绘和重排,提高页面的响应速度和动画的流畅度。

will-change属性可以接受多个属性值,表示将要改变的属性。例如,will-change: transform表示元素即将进行变形操作,will-change: opacity表示元素的透明度即将发生变化。

需要注意的是,will-change属性应在实际变化发生前的一段时间内被设置,以便浏览器有足够的时间进行准备和优化。另外,will-change属性并不会自动触发硬件加速,但它可以为浏览器提供一种优化渲染的提示。

此外,will-change属性的使用应谨慎,避免滥用。只有在明确知道元素即将发生某种变化,并且这种变化对性能有影响时,才应使用will-change属性。过度使用will-change属性可能会导致浏览器进行不必要的优化,反而降低性能

总的来说,will-change属性是一个用于优化渲染性能的CSS属性,通过提前告知浏览器元素即将发生的变化,使浏览器能够提前进行准备和优化,从而提高页面的响应速度和动画的流畅度

Demo:

css
 代码解读
复制代码

div {

    will-change: transform;

    width: 200px;

    background-color: red;

    margin: 0 auto;

}

通过上面的 css 设置,告诉浏览器:这个 div 的 transform 经常会变,浏览器会为该元素创建一个独立的图层,将这个图层标记为“即将变换”。这样,在进行布局和绘制时,浏览器就可以更高效地处理这个元素,而无需重新计算整个渲染树。 Browser-Layer-Demo2.png

五、绘制 - Paint

Browser-Paint.png

第四步的产出就是分层。第五步绘制阶段,会分别对每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来(类似 canvas 代码,告诉计算机从哪个点经过中间几个点,最后到哪个点绘制一条线,中间内容用什么颜色填充)

渲染主线程的工作到此为止,剩余步骤交给其他线程完成。

Browser-MainThread-duty.png

六、分块 - Tiling

完成绘制之后,主线程将每个图层的绘制信息提交给合成线程,剩余的工作将由合成线程完成。

Browser-Tilling.png

合成线程首先对每个图层分块(Tiling),将其划分为更多的小区域。合成线程类似一个任务调度者,它会从线程池中拿出多个线程来完成分块的工作。

Browser-Tilling-Worker.png

QA:浏览器渲染过程中分块的作用是什么?

  1. 提高渲染性能:当浏览器需要渲染一个复杂的网页时,如果一次性将所有内容加载并渲染出来,可能会消耗大量的计算资源和时间,通过网页分成多个较小的块(tiling)浏览器可以并行地加载和渲染这些块,从而充分利用计算资源,提高渲染性能。
  2. 优化内存占用:如果浏览器一次性加载和渲染整个网页,可能会消耗大量的内存。通过将网页分成多个图块,浏览器可以更好的管理内存,避免不必要的内存消耗
  3. 实现懒加载:通过分块,浏览器可以实现懒加载,即只有当某个图块进入视口可见区域时,才开始加载和渲染该图块。这样可以减少不必要的加载和渲染,提高页面的加载速度和性能
  4. 方便进行并行处理和异步操作:通过将网页分成多个图块,浏览器可以更容易地进行并行处理和异步操作。例如,在移动端设备上,可以利用 GPU 进行图块的并行渲染,提高渲染效率。

分块 tiling 技术是浏览器渲染过程中提高性能、优化内存使用和实现懒加载的重要手段之一。

七、光栅化

分块完成后会进入光栅化阶段。光栅化是将每个块变成位图。 Browser-Raster.png 合成线程将分块后的块信息交给 GPU 进程,以极高的速度完成光栅化,并优先处理靠近视口的块。 Browser-Raster-GPU.png

八、画 - Draw

合成线程计算出每个位图在屏幕上的位置,交给 GPU 进行最终的呈现。

Browser-Draw.png

合成线程拿到每个层(Layer)、每个块(Tile)的位图(bitmap)后,生成一个个指引(quad)信息。指引信息标识出每个位图应该画到屏幕上的哪个位置,此过程会考虑到旋转、缩放、变形等。

因为变形发生在合成线程,与渲染主线程无关,这也是 transform 效率高的本质原因。 合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕显示

完成过程如下

Browser-Full-Progress.png

reflow

Browser-Reflow.png 比如某个业务逻辑,导致我们用 js 脚本修改了某个节点的宽度、颜色,这其实修改的是 cssom,假设业务逻辑导致操作了 DOM,DOM 节点增删了,cssom、DOM 改变了,则会触发一系列的的流程,比如重新计算样式 style、布局(layout)、分层 layer、绘制 paint、分块 tiling、光栅化 raster、画 draw、系统调用 GPU 去真正显示。

当渲染树 render tree 中的一部分(或者全部)因为元素的尺寸、布局、颜色、隐藏等改变而需要重新构建,这就情况被称为回流。图上的 style 这部分。

回流发生时,浏览器会让渲染树中受到影响的部分失效,并重新构建这部分渲染树。完成回流 reflow 后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。

简单来说,reflow 就是计算元素在屏幕上确切的位置和大小并重新绘制。回流的代价远大于重绘。回流必定重绘,重绘不一定回流。

repaint

当渲染树 render tree 中的一些元素需要更新样式,但这些样式只是改变元素外观、风格,而不影响布局(位置、大小),则叫重绘。 简单来说,重绘就是将渲染树节点转换为屏幕上的实际像素,不涉及重新布局阶段的位置与大小计算

QA:为什么 transform 效率高? 假设在 css 中针对某个元素写了 transform: rotate(100deg),transform 既不会影响布局也不会影响绘制指令,它影响的是渲染流程的最后一个阶段,图上的 draw 阶段。由于 draw 阶段发生在合成线程中,不在渲染主线程,所以 transform 的任何变化几乎不会影响主线程。同样,不管主线程如何繁忙,都不影响合成线程中 transform 的变化。

Browser-Reflow.png