性能问题呈现给用户的感受往往是简单而直接的:加载资源缓慢、运行过程卡顿或响应交互迟缓等,当把这些问题呈现到前端工程师面前时,却是另一种系统级别复杂的图景。
从域名解析、TCP 建立连接到 HTTP 的请求与响应,以及从资源请求、文件解析到关键渲染路径等,每一个环节都有可能因为设计不当、考虑不周、运行出错而产生性能不佳的体验。作为前端工程师,为了能在遇到性能问题时快速而准确地定位问题所在,并设计可行的优化方案,熟悉前端页面的生命周期是一堂必修课。
本章就从一道常见的前端面试题开始,通过对此问题的解答,来分析前端页面生命周期的各个环节,并着重分析其中关键渲染路径的具体过程和优化实践,希望以此为基础帮读者建构一套完整知识框架的图谱,而后续章节的专题性优化,也都是对此生命周期中某个局部过程的优化分析。
一道前端面试题
大家在进行前端面试时,经常问这样一个问题:从浏览器地址栏输入URL后,到页面渲染出来,整个过程都发生了什么?
其实这个问题的回答可以非常细致,能从信号与系统、计算机原理、操作系统聊到网络通信、浏览器内核,再到 DNS 解析、负载均衡、页面渲染等,但这门课程主要关注前端方面的相关内容,为了后文表述更清楚,这里首先将整个过程划分为以下几个阶段。
浏览器接收到我们输入的URL到开启网络请求线程,这个阶段是在浏览器内部完成的。
解析URL后,如果是HTTP协议,则浏览器会新建一个网络请求线程去下载所需的资源,要明白这个过程需要先了解进程和线程之间的区别,以及目前主流的多进程浏览器结构。
单进程浏览器
说到底浏览器也只是一个运行在操作系统上的程序,那么它的运行单位就是进程,而早在 2008 年谷歌发布 Chrome 多进程浏览器之前,市面上几乎所有浏览器都是单进程的,它们将所有功能模块都运行在同一个进程中,其架构示意图如下图所示。
单进程浏览器存在许多的问题
多进程浏览器
https://blog.csdn.net/Whoopsina/article/details/123516144
浏览器把原先单进程内功能相对独立的模块抽离为单个进程处理对应的任务,主要分为以下几种进程。
这个阶段的主要工作分为:DNS解析和通信链路的建立
简单说就是首先发起请求的客户端浏览器要明确知道所要访问的服务器地址,然后简历通往该服务器地址的路径
URL解析其实就是将URL拆分为代表具体含义的字段,然后以参数的形式传入网络请求线程进行进一步处理,其中第一步就是DNS解析。它的主要目的就是通过查询URL的Host字段转化为网络中具体的IP地址,因为域名只是为了方便帮忙记忆的,IP地址才是访问服务器在网络中的“门牌号”。
DNS解析可以查找的过程简单的描述为
浏览器缓存 =》系统缓存 =》 hosts文件 =》 本地域名服务器 =》 根域名服务器 =》 COM顶级域名服务器和权限域名服务器
最终将所要访问的目标服务器IP地址返回本地主机,如果查询不到则会返回报错信息。
由此可见DNS解析是个很耗时的过程,如果解析的域名过多,势必会延缓首屏的加载时间,关于DNS解析过程的原理及优化方案可以查看我写的其他博客笔记。
在通过 DNS 解析获取到目标服务器 IP 地址后,就可以建立网络连接进行资源的请求与响应了。但在此之前,我们需要对网络架构模型有一些基本的认识,在互联网发展初期,为了使网络通信能够更加灵活、稳定及可互操作,国际标准化组织提出了一些网络架构模型:OSI 模型、TCP/IP 模型,二者的网络模型图示如下图所示。
OSI 是一种理论下的模型,它先规划了模型再填入协议,先制定了标准再推行实践,TCP/IP 充分借鉴了 OSI 引入的服务、接口、协议及分层等概念,建立了 TCP/IP 模型并广泛使用,成为目前互联网事实上的标准。
TCP连接
根据对网络模型的介绍,当使用本地主机脸上网线接入互联网后,数据链路层和网络层就已经打通了,而要想目标主机发起HTTP请求,就需要通过传输层建立端到端的连接。
传输层常见的协议有 TCP 协议和 UDP 协议,由于本章关注的重点是前端页面的资源请求,这需要面向连接、丢包重发及对数据传输的各种控制,所以接下来仅详细介绍 TCP 协议的“三次握手”和“四次挥手”。
由于 TCP 是面向有连接的通信协议,所以在数据传输之前需要建立好客户端与服务器端之间的连接,即通常所说的“三次握手”,三次握手的作用就是双方都能明确自己和对方的收发能力是正常的,具体过程分为如下步骤。
当用户关闭标签页或者请求完成后,TCP连接会进行“四次挥手”断开连接,具体过程如下:
只有连接建立成功后才可开始进行数据的传递,由于浏览器对于统一域名下并发的TCP连接有限制(大多数浏览器是6个),以及在1.0版本的HTTP协议中,一个资源的下载需要对应一个TCP的请求,这样的并发限制又会设计许多优化方案,我们将在后续章节中进行进一步介绍。
QA:为什么三次握手简历连接的TCP客户端最后还要发送一次确认呢?
主要防止已经失效的连接请求报文突然又传送到了服务器,导致重新建立起连接,从而产生错误
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络节点中滞留的时间太长了,由于 TCP 的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
QA:为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
QA:如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
当TCP连接建立好之后,便可以通过HTTP等协议进行前后端的同学,但是在实际的网络访问中,并非浏览器与确定IP地址的服务器之间直接通信,往往会在中间加入方向代理服务器
反向代理服务器
对需要提供复杂功能的网站来说,通常单一的服务器资源是很难满足期望的。一般采用的方式是将多个应用服务器组成的集群由反向代理服务器提供给客户端用户使用,这些功能服务器可能具有不同类型,比如文件服务器、邮件服务器及 Web 应用服务器,同时也可能是相同的Web服务部署到多个服务器上,以实现负载均衡的效果,反向代理服务器的作用如图所示。
反向代理服务器根据客户的请求,从后端服务器上获取资源后提供给客户端,单向代理服务器通常的作用如下:
后端处理流程
经反向代理收到请求后,具体的服务器后代处理流程大致如下:
HTTP 是建立在传输层 TCP 协议之上的应用层协议,在 TCP 层面上存在长连接和短连接的区别。所谓长连接,就是在客户端与服务器端建立的 TCP 连接上,可以连续发送多个数据包,但需要双方发送心跳检查包来维持这个连接。
短连接就是当客户端需要向服务器端发送请求时,会在网络层 IP 协议之上建立一个 TCP 连接,当请求发送并收到响应后,则断开此连接。根据前面关于 TCP 连接建立过程的描述,我们知道如果这个过程频繁发生,就是个很大的性能耗费,所以从 HTTP 的 1.0 版本开始对于连接的优化一直在进行。
在 HTTP 1.0 时,默认使用短连接,浏览器的每一次 HTTP 操作就会建立一个连接,任务结束则断开连接。
在 HTTP 1.1 时,默认使用长连接,在此情况下,当一个网页的打开操作完成时,其中所建立用于传输 HTTP 的 TCP 连接并不会断开关闭,客户端后续的请求操作便会继续使用这个已经建立的连接。如果我们对浏览器的开发者工具留心,在查看请求头时会发现一行 content:keep-alive。长连接并非永久保持,它有一个持续时间,可在服务器中进行配置。
而在 HTTP 2.0 到来之前,每一个资源的请求都需要开启一个 TCP 连接,由于 TCP 本身有并发数的限制,这样的结果就是,当请求的资源变多时,速度性能就会明显下降。为此,经常会采用的优化策略包括,将静态资源的请求进行多域名拆分,对于小图标或图片使用雪碧图等。
在 HTTP 2.0 之后,便可以在一个 TCP 连接上请求多个资源,分割成更小的帧请求,其速度性能便会明显上升,所以之前针对 HTTP 1.1 限制的优化方案也就不再需要了。
HTTP 2.0 除了一个连接可请求多个资源这种多路复用的特性,还有如下一些新特性
关于HTTP协议的具体内容,可以查看另外一篇博客笔记
在基于 HTTP 的前后端交互过程中,使用缓存可以使性能得到显著提升。具体的缓存策略分为两种:强缓存和协商缓存。
具体关于浏览器缓存可以查看另外一篇博客笔记
当我们经历了网络请求过程,从服务器获取到了所访问的页面文件后,浏览器如何将这些 HTML、CSS 及 JS 文件组织在一起渲染出来呢?
浏览器从获取 HTML 到最终在屏幕上显示内容需要完成以下步骤:
首先浏览器会通过解析HTML和CSS文件,来构建DOM(文档对象模型)和CSSOM(层叠样式表对象模型),为便于理解,我们用下面的HTML内容为例,来观察文档对象模型的构建过程。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关键路径渲染title>
<link rel="stylesheet" href="style.css">
head>
<body>
<p>你好<span>性能优化span>p>
<div>
<img src="avatar.jpg">
div>
body>
html>
浏览器接收读取到HTML文件,其实是文件根据指定编码(UTF-8)的原始字节,形如3C 62 6F 79 3E 65 6C 6F 2C 20 73 70…
。首先需要将字节转换成字符,即原本的代码字符串,接着再将字符串转化为W3C标准规定的令牌结构,所谓令牌就是HTML中不同标签代表不同含义的一组规则结构。然后经过词法分析将令牌转化成定义了属性和规则值的对象,最后将这些标签节点根据HTML表示的父子关系,连接成树形结构,如下图所示:
DOM树表示文档标记的属性和关系,但未包含其中各元素经过渲染后的外观呈现,这便是接下来CSSOM的职责了,与将HTML文件解析为文档对象模型的过程类型,CSS文件也会首先经历从字节到字符串,然后令牌华及词法分析后构建为层叠样式表对象模型。假设CSS文件内容如下:
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
最后构建的CSSOM树如图所示:
这两个对象模型的构建过程是会花费时间的,可以通过打开Chrome浏览器的开发者工具的性能选项卡,查看到对应过程的耗时情况,如图所示:
当完成文档对象模型和层叠样式对象模型的构建后,所得到的的其实是 描述最终渲染页面两个不同方面信息的对象:一个是展示的文档内容,另一个是文档对象对应的样式规则,接下来就需要将两个对象模型合并为渲染树,渲染树中只包含渲染课件的节点,该HTML文档最终生成的渲染树如图所示:
渲染绘制的步骤大致如下:
此处所举得例子较为简单,我们要明白执行构建渲染树、布局及绘制过程所需要的实际取决于实际文档的大小。文档越大,浏览器需要处理的任务就越多,样式也复杂,绘制需要的时间就越长,所以关键渲染路径执行快慢,将直接影响首屏加载时间的性能指标。
当首屏渲染完成后,用户在和网站的交互过程中,有可能通过js代码提供的用户操作接口更改渲染树的结构,一旦DOM结构发生变化,这个渲染过程就会重新执行一边。可见对于关键渲染路径的优化影响的不仅是首屏性能,还有交互性能。