浏览器是如何将HTML、CSS、JavaScript变成可视的页面的


浏览器是如何将 HTML、CSS、JavaScript 变成可视的页面的

渲染流程

渲染模块在执行过程中会被划分为很多子阶段 输入的HTML、CSS、JavaScript经过这些子阶段处理输出像素 这个处理流程叫做渲染流水线

image-20220516110029139

流水线分为以下几个阶段

  • 构建DOM
  • 样式计算
  • 布局阶段
  • 分层
  • 绘制
  • 分块
  • 栅格化
  • 合成

那既然流程这么复杂 如何避免搞混呢? 其实只需要理解以下三个部分就行 即

  • 开始时每个子阶段都有其输入的内容
  • 然后每个子阶段都有其处理过程
  • 最终每个子阶段会生成输出内容

1.构建 DOM 树

Q: 为什么要构建DOM树呢

A: 因为浏览器无法直接理解和使用HTML 所以需要将HTML转换成浏览器能够理解的DOM

接下来我们看一下DOM树的构建过程

image-20220516111753024

从图中可以看出 构建DOM树的输入内容是一个简单的HTML文件 经过HTML解析器解析 最终输出树状结构DOM

至此构建DOM树的阶段已经完成 接下来便是让DOM节点拥有正确的样式 这一阶段便是样式计算

2.样式计算

样式计算的目的是计算出DOM树节点中每个元素的具体样式 可分为三步完成

  1. css转换成浏览器能够理解的结构(styleSheets

    image-20220516113214615

    可以看出 CSS的来源主要有三种

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

    当浏览器接收到CSS文本时 会执行转换操作 将其转换成浏览器可以理解的styleSheets结构 该结构同时具有查询修改功能

    可以在 chrome 浏览器控制台输入document.styleSheets查看

    image-20220516113650127

  2. 转换样式表中的属性值 使其标准化

    第一步我们已经将CSS文本转换成可以理解的styleSheets结构了 接下来便是对属性值的表转化操作

    image-20220516114304703

    从图中看出 aqua被解析为rgb(0, 255, 255)……这就是标准化过程

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

计算具体属性涉及到CSS继承规则层叠规则

  • 继承:dom节点可以继承父节点的一些属性值
  • 层叠:定义了如何合并来自多个原的属性值的算法

一句话概括 样式计算阶段实在遵循CSS继承层叠规则下 计算输出每个DOM节点的样式并保存在ComputedStyle的结构内(可通过 Chrome 的 Computed 查看)

image-20220516114737531

3.布局阶段

布局阶段主要有两个任务

  1. 创建布局树
    1. 遍历DOM树中的所有可见节点 并把这些节点添加到布局中
    2. 不可见的节点会被布局树忽略掉
  2. 布局计算

4.分层

在完成布局阶段后 依旧不能着手绘制页面 因为为了更加方便地实现一些复杂的效果(比如 3D 变换 页面滚动等)渲染引擎为特定的节点生成专用的图层 并生成一颗对应的图层树 由这些图层叠加在一起构成了最终的页面图像 (可以使用 chrome 的 Layers 功能查看页面的图层构成以及调试图层渲染过程)

元素可以被单独提升为一个图层的情况:

  1. 拥有层叠上下文属性的元素 如定位属性 透明属性 CSS滤镜等
image-20220516153448835

2.需要剪裁的地方也会创建图层

例如div里面的文字较多且超出了显示区域 这个时候就产生了裁剪 渲染引擎会将裁剪文字内容的一部分用于显示在div区域 出现这种裁剪情况的时候 渲染引擎就会为文字部分单独创建一个层 如果出现滚动条 滚动条也会提升为单独的层

5.图层绘制

完成图层树的构建后 渲染引擎会对图层树中的每个图层进行绘制。

举个例子: 如果我们要画一副五星红旗 我们会怎么操作?通常 我们会进行这样的步骤

  • 绘制红色背景
  • 画一个大五角星
  • 画剩余的四个五角星

其实图层绘制的步骤和这个差不多 渲染引擎会将一个图层的绘制拆分成很多绘制指令 再将这些绘制指令按照顺序组成一个待绘制列表 如下图(可以通过 chrome 的 Layers 中的 document 层查看调试)

image-20220516154230624

6.栅格化操作

绘制列表只是用来记录绘制顺序和绘制指令的列表 而实际上操作是由绘制引擎中的合成线程来完成的(类似于待办事项里的代办只是用来记录的列表 而执行这些事项的是你自己) 那合成线程和渲染主线程之间有啥关系呢

image-20220516154601954

实际上 当图层的绘制列表准备好之后 主线程会将该列表commit给合成线程 而合成线程会将图层切割成多个图块 按照视口(简单理解为屏幕可视区域)附加的图块来优先生成位图 实际生成位图的操作是由栅格化来执行的。

Q: 为什么要优先将可视区附加的图块生成位图?

A: 当图层很长很大的时候 用户不一定会查看所有区域 如果一次性绘制所有图层的话 会产生太大的开销 也没有必要

Q: 什么是栅格化?

A: 将图块转化为位图(图块是栅格化的最小单位)

渲染进程维护了一个栅格化的线程池 所有图块栅格化都是在线程池内执行的

image-20220516155206479

栅格化通常会使用 GPU 来加速生成 使用 GPU 生成位图的过程称为快速栅格化 生成的位图被保存在 GPU 内存中

7.合成和显示

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

浏览器进程中的viz组件 根据DrawQuad命令 将页面内容绘制到内存中 然后将内存显示在屏幕上

到这里 经过一系列操作 HTML CSS JavaScript 文件就会变成屏幕可视的页面了。

8.总结

一个完整的渲染流程简述如下

  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构
  2. 渲染引擎将CSS样式表转换为styleSheets 并计算出DOM节点的具体样式
  3. 创建布局树 计算元素的布局信息
  4. 对布局树进行分层 生成分层树
  5. 为每个图层生成绘制列表 并将其提交到合成进程
  6. 合成进程将图层分成图块 并在栅格化线程池将其转换为位图
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程
  8. 浏览器根据DrawQuad生成页面并显示到显示器上

相关概念

1.重排

image-20220516160105558

当修改了元素的几何属性(宽度 高度等)会使浏览器重新布局 解析之后的一系列子阶段 就叫重排 重排后需要更新完整的渲染流水线 所以开销是最大的

重排一定会导致重绘

2.重绘

更改元素的背景色等 (不会触发布局阶段 没有引起集合位置的变化 )直接进入了绘制阶段 然后执行之后的一系列子操作 就叫重绘

image-20220516160545832

Q:重排快还是重绘快?

A:重绘省去了布局和分层阶段 所以执行效率更快

重绘不一定导致重排

3.直接合成阶段

当做了既不导致重排 也不导致重绘的操作 就跳过布局和绘制 只执行后续操作 称为直接合成

image-20220516160821428

例如 transform可以避免重排和重回 直接在非主线程上执行合成动画操作(这也是效率最高的 因为没有占用主线程的资源 也没有重排和重绘)

Q: 如何减少重排和重绘?

A: 触发重排和重绘的操作尽量放在一起(如修改高度或者边距) 通过虚拟dom 层计算出操作总得差异 一起提交给浏览器(例如使用createdocumentfragment来汇总appenddom 来减少触发重排重绘次数)

结语:相信看了上面的文章 大家对于浏览器是如何将 HTML CSS JavaScript 文件处理并生成我们看到的页面这个过程有了更深的理解 文章若有错误之处 请大家在评论区指出指正!笔者最近正在学习浏览器的工作原理及实践的相关内容 感兴趣的小伙伴可以点波关注一起学习 后续会继续更新相关系列内容!


文章作者: olddog
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 olddog !
  目录