DNS 查询
首先,是去寻找页面资源的位置。如果导航到https://example.com
, 假设HTML页面被定位到IP地址为93.184.216.34
的服务器。如果以前没有访问过这个网站,就需要进行DNS查询。
浏览器向域名服务器发起DNS查询请求,最终得到一个IP地址。第一次请求后,这个IP地址可能会被缓存一段时间,这样后续对这个网址的请求中可以通过从缓存里面检索IP地址而不需要再通过域名服务器来进行查询。
每个主机名在页面加载时通常只需要进行一次DNS查询。但是,对于页面指向的不同的主机名
,则需要多次DNS查询。如果字体、图像、脚本、广告都有不同的主机名,则需要对每一个主机名进行DNS查询。
注意:
对于移动网络,DNS查询可能存在性能问题。当一个用户使用移动网络时,所有DNS查询必须从手机发送到基站,然后再到达DNS服务器。手机、信号塔、域名服务器之间的距离会显著增加延迟。
TCP握手
一旦获取到服务器IP地址, 浏览器就会通过TCP三次握手
与服务器建立连接,双方之间建立通信。三次握手的目的是确保服务器和客户端双方都能确认对方有发送和接收的能力。对于通过HTTPS建立的安全链接,还需要额外的握手,即TLS协商。协商网络 TCP 套接字连接的一些参数,以及后续的密钥的传输和证书的验证。(HTTPS - MDN Web 文档术语表:Web 相关术语的定义 | MDN)
响应
一旦建立了和 web 服务器的连接,浏览器就会代表用户发送一个初始的HTTP Get
请求,一旦服务器收到请求,将使用相关的响应头和HTML内容进行回复。
初始请求的响应包含所接受数据的第一个字节。首字节时间(TTFB)
是用户通过点击链接进行请求与收到第一个 HTML 数据包之间的时间。第一个内容分块通常是 14KB 的数据。
为什么是14kb?
- 性能优化:14KB 是一个折中的选择,既能够提供足够的数据量来减少多次往返的时间,又不会因为数据量过大而引起网络拥塞。
- 兼容性:大多数现代操作系统和网络设备都能很好地处理 14KB 的数据块,这使得 14KB 成为一个广泛接受的标准。
- 用户体验:对于大多数 Web 应用来说,14KB 的数据量通常足以包含一个网页的关键部分,如 HTML 头部、CSS 样式表和一些 JavaScript 代码,从而加快页面的首次渲染速度
拥塞控制/TCP慢启动
传输过程中,TCP 包被分割成段。由于 TCP 保证了数据包的顺序,因此服务器在发送一定数量的分段后,必须从客户端接收一个 ACK 包的确认。
如果服务器在发送每个分段之后都等待 ACK,那么客户端将频繁地发送 ACK,并且可能会增加传输时间,即使在网络负载较低的情况下也是如此。
另一方面,一次发送过多的分段会导致在繁忙的网络中客户端无法接收分段并且长时间地只会持续发送 ACK,服务器必须不断重新发送分段的问题。
为了平衡传输分段的数量,TCP 慢启动
算法用于逐渐增加传输数据量,直到确定最大网络带宽,并在网络负载较高时减少传输数据量。
解析
一旦浏览器收到第一个数据分块,它就可以开始解析收到的信息。“解析”是浏览器将通过网络接收的数据转换为
DOM 和 CSSOM 的步骤,通过渲染器在屏幕上将它们绘制成页面。
构建DOM树
第一步是处理 HTML 标记并构造 DOM 树。
DOM 树描述了文档的内容。元素是第一个标签也是文档树的根节点。树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM 节点的数量越多,构建 DOM 树所需的时间就越长。
当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个 CSS 文件时,解析也可以继续进行,但是对于 <script>
标签(特别是没有 async
或者 defer
属性的)会阻塞渲染并停止 HTML 的解析。
预加载扫描器
浏览器构建 DOM 树时,这个过程占用了主线程。同时,预加载扫描器会解析可用的内容并请求高优先级的资源,如 CSS、JavaScript 和 web 字体。多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用时才去请求。它将在后台检索资源,而当主 HTML 解析器解析到要请求的资源时,它们可能已经下载中了,或者已经被下载。预加载扫描器提供的优化减少了阻塞。
构建CSSOM树
第二步是处理 CSS 并构建 CSSOM 树。CSS 对象模型和 DOM 是相似的。DOM 和 CSSOM 是两棵树。它们是独立的数据结构。浏览器将 CSS 规则转换为可以理解和使用的样式映射。浏览器遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树。
与 HTML 类似,浏览器需要将接收到的 CSS 规则转换为可处理的格式。因此,它重复了 HTML 到对象的过程,但这次是针对 CSS。
渲染
渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的 CSSOM 树和 DOM 树组合成一个渲染树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在 GPU 而不是 CPU 上绘制屏幕的一部分来提高性能,从而释放主线程。
样式
关键呈现路径的第三步是将 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
的元素,还有一些其他元素。这些节点将与子节点一起绘制到它们自己的层上,除非子节点由于上述一个(或多个)原因需要自己的层。
分层确实可以提高性能,但在内存管理方面成本较高,因此不应作为 Web 性能优化策略的过度使用。
合成
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
当页面继续加载资源时,可能会发生回流,回流会触发重新绘制和重新合成。如果我们定义了图像的大小,就不需要重新绘制,只需要绘制需要重新绘制的层,并在必要时进行合成。但如果没有定义图像大小,服务器获取图像后,渲染过程将返回到布局步骤并从那里重新开始。
交互
一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。如果加载包括正确延迟加载的 JavaScript,并且仅在 onload
事件触发后执行,那么主线程可能会忙于执行脚本,无法用于滚动、触摸和其他交互操作。
可交互时间(TTI)是测量从第一个请求导致 DNS 查询和 SSL 连接到页面可交互时所用的时间——可交互是在首次内容绘制之后页面在 50ms 内响应用户的交互。如果主线程正在解析、编译和执行 JavaScript,则无法及时(小于 50ms)响应用户交互。
在我们的示例中,可能图像加载很快,但 anotherscript.js
文件的大小可能是 2MB,而且用户的网络连接很慢。在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。这不是一个好的用户体验。避免占用主线程,如下面的网页测试示例所示:
在本例中,DOM 内容加载过程花费了超过 1.5 秒的时间,主线程在这段时间内完全被占用,对单击事件或屏幕点击没有响应。