在工作当中不知道你有没有遇到这样的情况,团队对首屏时间的要求是 1200ms,目前首屏时间长达 2s,离要求还有不小的差距。为此,你精简了首屏内容,合并了请求资源,对图片尺寸也进行了压缩优化,但最后的首屏时间还是没有降下来。为什么?
实际上,想要对 Web 前端进行性能优化,除了了解性能体系、关键性能指标之外,还需要了解页面加载全过程。通过这个过程,我们可以找到其中影响性能的关键点、瓶颈点,接下来才好有的放矢。
那,页面加载大致过程是怎样的呢?
我们可以假设一个人正在上网,当他在浏览器输入 URL 并回车后,为了把 URL 解析为 IP 地址,浏览器会向 DNS 服务器发起 DNS 查询,获取 IP 地址。然后浏览器通过 IP 地址找到目标服务器,发起 TCP 三次握手和 TLS 协商,从而建立起 TCP 连接。
在建立连接后,浏览器就可以发起 HTTP 请求,而服务端接收到后,对请求进行响应。浏览器从响应结果中拿到数据,并进行解析和渲染,最后在用户面前就出现了一个网页。
以上的整个过程大致可以分为三个阶段:客户端发起请求阶段、服务端数据处理请求阶段、客户端页面渲染阶段。下面我就根据这三个阶段来和你介绍下 Web 前端都有哪些性能瓶颈点。
客户端请求阶段的瓶颈点
客户端发起请求阶段,是指用户在浏览器输入 URL,经过本地缓存确认是否已经存在这个网站,如果没有,接着会由 DNS 查询从域名服务器获取这个 IP 地址,接下来就是客户端通过 TCP 的三次握手和TLS协商向服务器发起 HTTP 请求建立连接的过程。
在这个过程中,本地缓存、DNS查询、HTTP 请求很容易成为影响前端性能的瓶颈点。
本地缓存
为什么说本地缓存会成为前端性能的瓶颈点之一呢?
我们知道,本地缓存可以让静态资源加载更快,当客户端发起一个请求时,静态资源可以直接从客户端中获取,不需要再向服务器请求。而想要让本地缓存发挥这个作用,就需要先在服务器上进行配置。
但在实际当中,许多前端同学往往会忽视这点。这会出现一个什么情况呢?
以 58 同城的列表页项目为例,在客户端请求阶段,DNS 查询时间大概是 385 ms,TCP 三次握手及 TLS 协商时间 436 ms,数据返回 412 ms。一个请求下来大约是 1233 ms,这还是强网(WIFI/4G)情况下。如果是弱网(3G/2G)情况,一个请求连接都需要 2s 。但使用缓存的话,几乎可以说是几毫秒内完成请求,对比非常明显。
怎么实现本地缓存呢?
本地缓存一般包括强缓存和协商缓存两种形式。
-
强缓存是指浏览器在加载资源时,根据请求头的 expires 和 cache-control 判断是否命中客户端缓存。如果命中,则直接从缓存读取资源,不会发请求到服务器,反之还需要走完整的资源请求流程。
-
协商缓存是指,浏览器会先发送一个请求到服务器,通过 last-modified 和 etag 验证资源是否命中客户端缓存。如果命中,服务器会将这个请求返回,但不会返回这个资源的数据,依然是从缓存中读取资源。 如果没有命中,无论是资源过期或者没有相关资源,都需要向服务器发起请求,等待服务器返回这个资源。
DNS 查询
DNS 之所以会成为前端性能瓶颈点,是因为每进行一次 DNS 查询,都要经历从手机到移动信号塔,再到认证 DNS 服务器的过程。这中间需要很长的时间。但用户是不想等待的。
想要节省时间,一个办法就是让 DNS 查询走缓存。幸好浏览器提供了 DNS 预获取的接口,我们可以在打开浏览器或者 WebView 的同时就进行配置。这样真正请求时,DNS 域名解析可以检查一下浏览器缓存,一旦缓存命中,就不需要去 DNS 服务器查询了。
HTTP 请求
在 HTTP 请求阶段,最大的瓶颈点来源于请求阻塞。所谓请求阻塞,就是浏览器为保证访问速度,会默认对同一域下的资源保持一定的连接数,请求过多就会进行阻塞。
那么,浏览器同域名的连接数限制是多少呢?一般是 6 个。如果当前请求书多于 6 个,只能 6 个并发,其余的得等最先返回的请求后,才能做下一次请求。
所以我们在项目之初,做一些域名规划很重要。我们可以先看看当前页面需要用到哪些域名,最关键的首屏中需要用到哪些域名,规划一下这些域名发送的顺序。
除了域名规划,为了解决同域名下的阻塞问题,还可以做域名散列,通过不同的域名,增加请求并行连接数。常见做法是,将静态服务器地址 pic.google.com,做成支持 pic0-5 的 6 个域名,每次请求时随机选一个域名地址进行请求。因为有 6 个域名同时可用,最多可以并行 36 个连接。当然,这个域名个数不是越多越好,太分散的话,又会涉及多域名之间无法缓存的问题。
服务端数据处理阶段的瓶颈点
服务端数据处理阶段,是指 WebServer 接收到请求后,从数据存储层取到数据,再返回给前端的过程。
具体来说,服务端程序接收到 HTTP 请求后,会做一些请求参数处理以及权限校验。校验完成后,它会将请求参数发送到数据存储服务。然后服务端程序会从数据存储中取到数据,进行数据加工聚合处理,最后再通过 jsonp 或者 ajax 接口返回给前端。
这个过程中的瓶颈点,就在于是否做了数据缓存处理、是否做了 Gip 压缩,以及是否有重定向。
其中,Gzip 压缩是一种压缩技术,服务器端通过使用 Gzip,传输到浏览器端的文本类资源(有别于图片等二进制等资源)的大小可以变为原来的 1/3 左右。因此通过 Gzip 压缩,资源的下载速度会快很多,能大大提升页面的展示速度。
因为多数情况下,运维团队都会默认开启 Gzip 压缩,所以在这里我就不展开介绍了,接下来我会重点介绍一下数据缓存和重定向,这两个瓶颈点。
数据缓存
数据缓存分为两种:借助 Service Worker 的数据接口缓存、借助本地存储的接口缓存和CDN(Content Delivery Network,内容分发网络)
其中 Service Worker 是浏览器的一个高级属性,本质上是一个请求代理层,它存在的目的就是拦截和处理网络数据请求。如果没有 Service Worker,请求每次直接落到 WebServer 上,需要走一次后端数据存取链路的流程,这会延长页面打开时间。
借助本次存储的接口缓存是指,在一些对数据时效性要求不高的页面,第一次请求到数据后,程序将数据存储到本地存储(store 或者 localStorage、甚至客户端本身的存储),下一次请求的时候,先去缓存里面取将取数据,如果没有的话,再向服务器发起请求。
所谓 CDN,它的基本思路是,通过在网络各处放置节点服务器,构造一个智能虚拟网络,将用户的请求导向离用户最近的服务节点上
比如,一个深圳的用户想要访问京东电商首图的资源,如果没有使用 CDN ,图片服务器有可能在北京。但如果使用了CDN 的话,因为 CDN 的节点遍布全国,它就可以选择距离深圳最近的节点返回首图数据。
为什么数据缓存会成为性能瓶颈点呢?这是因为每请求一次数据接口,需要从客户端到后端服务器,再到更后端的数据存储层,一层一层返回数据,最后再给到客户端,耗时很长,如果能够减少一次这个请求,为首屏时间争取了宝贵的时间。