上篇回顾
前端必知之:从url到dom的中间爱恨情仇——系列二:响应阶段
在上篇中,我们详细介绍了从 URL 到 DOM 的第二个阶段:响应阶段。我们首先讨论了响应阶段返回的六种数据类型,其中包括常用的 JSON 数据等。接着,我们深入探讨了缓存的概念、流程,以及一些专有名词和常见问题。
上面主要涉及计算机基础原理知识,而在本篇中,我们将专注于与前端更相关的内容:如何将一个 HTML 页面渲染到浏览器上,并展示出动效和交互!
渲染阶段
一旦与Web服务器建立连接,浏览器即代表用户发送初始的HTTP GET请求。对于大多数网站而言,此请求通常指向一个HTML文件。服务器收到请求后,将利用相关的响应头和HTML内容进行回复,典型的响应即为返回一个HTML文件,大概格式如下:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>返回的页面</title>
<link rel="stylesheet" href="style.css" />
<script src="returnScript.js"></script>
</head>
<body>
<h1 class="heading">返回页面</h1>
<p>含有<a href="https://example.com/about">链接</a>的段落。</p>
<div>
<img src="returnImage.jpg" alt="图像描述" />
</div>
<script src="otherscript.js"></script>
</body>
</html>
一旦浏览器接收到数据,它就开始解析收到的数据。
这里的解析是浏览器将收到的数据转换为DOM和CSSOM,并通过渲染器将它们绘制成页面。
HTML解析
目的:处理HTML标记并构建DOM树
解析器根据DON树的层级结构以及对应的HTML标记,进行逐步解析并构建DOM树:
且DOM 节点的数量越多,构建 DOM 树所需的时间就越长。
CSS解析
目的: 处理CSS并构建CSSOM树
浏览器将CSS规则转换为可以理解和使用的样式映射。浏览器遍历CSS中的每个规则集,根据CSS选择器创建具有父、子和兄弟关系的节点树;
CSSOM 树包括来自用户代理样式表的样式。浏览器从适用于节点的最通用规则开始,并通过应用更具体的规则递归地优化计算的样式。换句话说,它级联属性值。
构建CSSOM非常快,在开发人员工具中的“重新计算样式”显示解析CSS、构建CSSOM树和递归计算计算样式所需的总时间;在 web 性能优化方面,它是可轻易实现的,因为创建 CSSOM 的总时间通常小于一次 DNS 查询所需的时间。
渲染树构建
目的: 将 DOM 和 CSSOM 组合成渲染树
计算样式树或渲染树的构建从 DOM 树的根开始,遍历每个可见节点。
不会被显示的元素,如 <head>元素及其子元素,以及任何带有 display: none 的节点,如用户代理样式表中的 script { display: none; },都不会包含在渲染树中,因为它们不会出现在渲染输出中。应用了 visibility: hidden 的节点会包含在渲染树中,因为它们会占用空间。由于我们没有给出任何指令来覆盖用户代理默认值,因此上述代码示例中的 script 节点不会包含在渲染树中。
每个可见节点都应用了 CSSOM 规则。渲染树包含所有可见节点的内容和计算样式,将所有相关样式与 DOM 树中的每个可见节点匹配起来,并根据CSS 级联,确定每个节点的计算样式。
节点布局
目的: 在渲染树上运行布局以计算每个节点的几何体
布局是确定呈现树中所有节点的尺寸和位置,以及确定页面上每个对象的大小和位置的过程。
重排是后续过程中对页面的任意部分或整个文档的大小和位置的重新计算。
渲染树构建完毕后,浏览器就开始布局。
渲染树标识了哪些节点会显示(即使不可见)及其计算样式,但不标识每个节点的尺寸或位置。为了确定每个对象的确切大小和位置,浏览器会从渲染树的根开始遍历。
在网页上,大多数东西都是一个盒子。不同的设备和不同的桌面设置意味着无限数量的不同视区大小。在此阶段,根据视口大小,浏览器将确定屏幕上所有盒子的大小。以视口大小为基础,布局通常从 body 开始,设置所有 body 后代的大小,同时给不知道其尺寸的替换元素(例如图像)提供占位符空间,空间大小以相应元素盒模型的属性为准。
第一次确定每个节点的大小和位置称为布局。
随后对节点大小和位置的重新计算称为重排。
页面渲染
目的: 将各个节点绘制到屏幕上
第一次的绘制被称首次有意义的绘制。
在绘制或光栅化阶段,浏览器将在布局阶段计算的每个盒子转换为屏幕上的实际像素。绘制涉及将元素的每个可见部分绘制到屏幕上,包括文本、颜色、边框、阴影以及按钮和图像等替换元素。浏览器需要以超快的速度执行这个过程。
为了确保平滑滚动和动画效果,包括计算样式、回流和绘制等占用主线程的所有操作,必须在不超过 16.67 毫秒的时间内完成。在 2048 x 1536 分辨率下,iPad 需要将超过 314.5 万个像素绘制到屏幕上。这是非常多的像素,必须要非常快速地绘制出来。为了确保重绘能够比初始绘制更快地完成,绘制到屏幕的操作通常被分解成几个图层。如果发生这种情况,浏览器则需要进行合成。
绘制可以将布局树中的元素分解为多个层。将内容提升到 GPU 上的层(而不是 CPU 上的主线程)可以提高绘制和重新绘制性能。有一些特定的属性和元素可以实例化一个层,包括 <video> 和 <canvas>,任何 CSS 属性为 opacity 、3D transform、will-change 的元素,还有一些其他元素。这些节点将与子节点一起绘制到它们自己的层上,除非子节点由于上述一个(或多个)原因需要自己的层。
补充
在以上的五个阶段中间,还有一些别的阶段需要补充,它们掺杂在这五个阶段中间:
预加载扫描
位于阶段: HTML解析,构建DOM树时;
浏览器构建 DOM 树时,这个过程占用了主线程。同时,预加载扫描器会解析可用的内容并请求高优先级的资源,如 CSS、JavaScript 和 web 字体。
多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用时才去请求。它将在后台检索资源,而当主 HTML 解析器解析到要请求的资源时,它们可能已经下载中了,或者已经被下载。
预加载扫描器提供的优化减少了阻塞。
CSS样式不会阻塞HTML的解析或者下载,但是它会阻塞JavaScript,因为JavaScript经常用于查询元素的CSS属性。
JavaScript编译
位于阶段: CSS解析,构建CSSOM树时;
在解析 CSS 和创建 CSSOM 的同时,包括 JavaScript 文件在内的其他资源也在下载(这要归功于预加载扫描器)。
JavaScript 会被解析、编译和解释。脚本被解析为抽象语法树。有些浏览器引擎会将抽象语法树输入编译器,输出字节码。这就是所谓的 JavaScript 编译。
大部分代码都是在主线程上解释的,但也有例外,例如在 web worker 中运行的代码。
构建无障碍树
位于阶段: CSS解析,构建CSSOM树时;
浏览器还构建辅助设备用于分析和解释内容的无障碍树。无障碍对象模型(AOM)类似于 DOM 的语义版本。当 DOM 更新时,浏览器会更新辅助功能树。辅助技术本身无法修改无障碍树。
在构建 AOM 之前,屏幕阅读器无法访问内容。
合成
位于阶段: 页面渲染时;
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
当页面继续加载资源时,可能会发生回流(回想一下我们迟到的示例图像),回流会触发重新绘制和重新合成。
如果我们定义了图像的大小,就不需要重新绘制,只需要绘制需要重新绘制的层,并在必要时进行合成。但我们并没有定义图像大小!所以从服务器获取图像后,渲染过程将返回到布局步骤并从那里重新开始。
交互
一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。如果加载包括正确延迟加载的 JavaScript,并且仅在 onload 事件触发后执行,那么主线程可能会忙于执行脚本,无法用于滚动、触摸和其他交互操作。
可交互时间(TTI)是测量从第一个请求导致 DNS 查询和 SSL 连接到页面可交互时所用的时间——可交互是在首次内容绘制之后页面在 50ms 内响应用户的交互。如果主线程正在解析、编译和执行 JavaScript,则无法及时(小于 50ms)响应用户交互。
总结
本篇我们总结了浏览器渲染页面各个阶段,以及一些补充步骤,帮助大家更好的理解浏览器渲染页面的过程:
1. HTML解析
2. 预加载器扫描
3. CSS解析
4. JavaScript编译
5. 构建无障碍树
6. 渲染树构建
7. 节点布局
8. 合成
9. 页面渲染
10. 交互