图解浏览器渲染引擎:揭秘网页绘制的魔法世界

我之前一直有个疑问,为什么编写的 HTML、CSS、JavaScript 等文件,经过浏览器运行之后就会显示出我们想要的页面呢?他们是如何转化为页面的?这背后的原理是什么??经过一番折腾,终于找到了答案,也想跟大家分享一下~

其实,这个过程是浏览器的渲染进程来实现的。渲染器进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以交互的网页。

由于渲染机制比较复杂,所以渲染模块在执行过程中会被划分为很多子阶段 ,输入的静态资源经过这些子阶段,最后输出页面。我们将一个处理流程称为渲染流水线,其大致流程如下图所示:

总的来说,可分为这几个阶段 DOM树构建CSSOM树构建渲染树构建页面布局页面绘制

下面就分别来看看这些过程都做了哪些操作

一、DOM树构建

可能会有小伙伴会问为什么要构建DOM树呢? 因为浏览器是无法直接理解和使用 HTML ,所以需要将 HTML 转化为浏览器能够理解的结构,也就是 DOM 树。

在了解 DOM 树构建之前,我们先来看看,什么是🌲,下面我画了 2 幅图,来方便大家理解

相信大家都能一眼看出来,只有第一个是 结构,树是由结点或顶点和边组成的且不存在着任何环的一种数据结构 ,而右边的图,很明显存在了一个闭环,所以它不是一个 结构

回归正题,那么什么是 DOM 树 呢? 在页面里,每个 HTML标签 都会被浏览器解析成一个个文档对象。HTML 本质上就是一个嵌套结构,在解析的时候,就会把每一个文档对象用一个树形结构组织起来,最后挂在document上,这种组织方式就是 HTML 最基础的结构------ 文档对象模型(DOM) ,这棵树上的文档对象就叫做DOM节点,DOM 是浏览器的页面内部表示,也是 Web 开发人员可以通过 JavaScript 进行交互的数据结构和 API。

相信大家都了解什么是 DOM 树啦,接下来,我们来看下它是怎么构建的:

其主要过程,可以通过这幅图来描述:

HTML解析器HTML 字节流转换为 DOM 结构,其转化过程如下:

1. 字符流 → 词(token)

HTML结构会首先通过分词器将字节流拆分为词(token)。Token分为Tag Token文本 Token。 例子:

css 复制代码
<p>hello world</p>

可以看到,Tag Token 又分 StartTag 和 EndTag, <p> 就是 StartTag , </p> 就是 EndTag,分别对应图中的蓝色和红色块,文本 Token 对应绿色块。

2. 词(token)→ DOM树

接下来就需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。这个过程是通过栈结构来实现的,这个栈主要用来计算节点之间的父子关系,上面步骤中生成的token会按顺序压入栈中

下面来看看这的 Token 栈是如何工作的,

例子:

开始时, HTML 解析器会创建一个根为 document 的空的 DOM 结构,同时将 StartTag document 的 Token 压入中,然后再将解析出来的第一个 StartTag html 压入栈中,并创建一个 html 的 DOM 节点,添加到 document 上,这时 Token 栈和 DOM 树如下:

接下来 bodydiv 标签也会和上面的过程一样,进行入栈操作:

随后就会解析到 div 标签中的文本Token,渲染引擎会为该 Token 创建一个文本节点,并将该 Token 添加到 DOM 中,它的父节点就是当前 Token 栈顶元素对应的节点:

接下来就是第一个EndTag div,这时 HTML 解析器会判断当前栈顶元素是否是 StartTag div,如果是,则从栈顶弹出 StartTag div,如下图所示: 依次不断进栈出栈,最终的结果如下:

那么 DOM 树 就构建完毕啦!❀❀❀

二、CSSOM树构建

同样,浏览器也是无法直接理解CSS代码的,需要将其解析成浏览器可以理解的CSSOM树。实际上。浏览器在构建 DOM 树的同时,如果样式也加载完成了,那么 CSSOM 树也会同步构建。

📢 在将CSS转化为树形对象之前,还需要将样式表里所有值转化为浏览器渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。比如,当遇到以下CSS样式:

less 复制代码
body { font-size: 2em,font-weight: bold,color:green; }

经过标准化的过程,上面的代码会变成这样:

less 复制代码
body { font-size: 32px,font-weight: 700,color:rgb(0, 128, 0); }

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

例子:

js 复制代码
<head>
  <link href="./style.css">
  <style>
    .box {
      width: 100px;
      height: 50px;
      background: red;
    }

    .content {
      font-size: 25px;
      line-height: 25px;
      margin: 10px;
    }
  </style>
</head>

<body>
  <div class="box">
    <div>hello</div>
  </div>
  <p style="color: blue" class="content"> <span>hello world</span>
  <p style="display: none;">浏览器</p>
  </p>
</body>

</html>

最终构建的 CSSOM 树 大致如下:

三、渲染树构建

DOM 树CSSOM 树 都渲染完成之后,就会进入渲染树的构建阶段。渲染树就是 DOM 树和 CSSOM 树的结合,会得到一个可以知道每个节点会应用什么样式的数据结构。

构建出的渲染树大致如下:

需要注意的是,DOM树中不可见的节点不会包含到渲染树中。为了构建渲染树,浏览器上大致做了如下工作:遍历DOM树中所有可见节点,并把这些节点加到布局中,而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 p.p 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包含进渲染树中。如果给元素设置了visibility: hidden;属性,那这个元素会出现在渲染树中,因为具有这个样式的元素是需要占位的,只不过不需要显示出来。

四、页面布局

经过上面的步骤,一棵渲染树就生成啦,这棵树就是展示页面的关键。到现在为止,需要渲染的所有节点之间的结构关系及其样式信息就确定好了。接下来就进入页面的布局过程。

通过计算渲染树上每个节点的样式,能得出来每个元素所占空间的大小和位置。当有了所有元素的大小和位置后,就可以在浏览器的页面区域里去绘制元素的边框了。那么,这个过程就是布局 。这个过程中,浏览器对渲染树进行遍历,将元素间嵌套关系以盒模型的形式写入文档流:

盒模型在布局过程中会计算出元素确切的大小和定位。计算完毕后,相应的信息被写回渲染树上,就形成了布局渲染树

五、页面绘制

1. 构建图层

经过布局,每个元素的位置和大小就有了,那下面是不是就该开始绘制页面了?答案是否定的,因为页面上可能有很多复杂的场景,比如3D变化页面滚动、使用z-index进行z轴的排序等。所以,为了实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。

那什么是图层呢?相信用过 Photoshop 的小伙伴对图层并不陌生。我们也可以在Chrome浏览器的开发者工具中,选择Layers标签,就可以看到页面的分层情况,以掘金为例,其分层情况如下:

可以看到,渲染引擎给页面分了很多图层,这些图层会按照一定顺序叠加在一起,就形成了最终的页面。 通常情况下,并不是渲染树上的每个节点都包含一个图层,如果一个节点没有对应的图层,那这个节点就会属于其父节点的图层。那什么样的节点才能让浏览器引擎为其创建一个新的图层呢?需要满足以下其中一个条件:

(1)拥有层叠上下文属性的元素

对于上图,由上到下分别是:

  • 背景和边框:建立当前层叠上下文元素的背景和边框。
  • 负的z-index:当前层叠上下文中,z-index属性值为负的元素。
  • 块级盒:文档流内非行内级非定位后代元素。
  • 浮动盒:非定位浮动元素。
  • 行内盒:文档流内行内级非定位后代元素。
  • z-index:0:层叠级数为0的定位元素。
  • 正z-index:z-index属性值为正的定位元素。

(2)需要裁剪的元素

什么是裁剪呢?假如有一个固定宽高的div盒子,而里面的文字较多超过了盒子的高度,这时就会产生裁剪,浏览器渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。当出现裁剪时,浏览器的渲染引擎就会为文字部分单独创建一个图层,如果出现滚动条,那么滚动条也会被提升为单独的图层。

2. 绘制图层

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,下面就来看看渲染引擎是怎么实现图层绘制的。

  • 渲染引擎在绘制图层时,会把一个图层的绘制分成很多绘制指令,然后把这些指令按照顺序组成一个待绘制的列表
主线程的光栅和合成

当确定了绘制顺序,主线程就会将该信息提交给合成器线程。然后合成器线程对每一层进行光栅化。图层可能像页面的整个长度一样大,甚至超过视口区,因此合成器线程会将它们分成图块并将每个图块发送到光栅线程。光栅线程对每个图块进行光栅化并将其存储在 GPU 内存中。

简单来说,将页面分解成多个图层的操作就成为分层, 最后将这些图层合并到一层的操作就成为合成

合成是一种将页面的各个部分分成图层分别光栅化并在渲染引擎中的合成线程的单独线程中合成为页面的技术。如果发生滚动,由于图层已经光栅化,它所要做的就是合成一个新帧。并且可以通过移动图层并合成新帧以相同的方式实现动画。

那什么叫光栅化呢?简单来说,就是将这些信息转换成屏幕上的像素。我们可以用下面的动画所示:

一旦图块被光栅化,合成线程就会收集称为"绘制四边形" 的图块信息来创建合成器框架

然后,合成器框架通过 IPC 提交给浏览器进程。此时,可以从 UI 线程添加另一个合成器框架以进行浏览器 UI 更改,或者从其他渲染器进程添加以进行扩展。这些合成器帧被发送到 GPU 以将其显示在屏幕上。如果出现滚动事件,合成器线程将创建另一个合成器帧发送到 GPU。

最终,内存显示在屏幕上,这样就完成了页面的绘制。

至此,整个渲染流程就完成了,其过程总结如下:

  1. 将HTML内容构建成DOM树;
  2. 将CSS内容构建成CSSOM树;
  3. 将DOM 树和 CSSOM 树合成渲染树;
  4. 根据渲染树进行页面元素的布局;
  5. 对渲染树进行分层操作,并生成分层树;
  6. 为每个图层生成绘制列表,并提交到合成线程;
  7. 合成线程将图层分成不同的图块,并通过栅格化将图块转化为位图;
  8. 合成线程给浏览器进程发送绘制图块指令;
  9. 浏览器进程会生成页面,并显示在屏幕上。
相关推荐
再吃一根胡萝卜2 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜2 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong4 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1126 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js