从输入URL到页面加载完成的整个过程
- 首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
- 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
- TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据(如果使用HTTPS)
- 数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
- 首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
- 浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
- 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到
script
标签的话,会判断是否存在async
或者defer
,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。 - 初始的 HTML 被完全加载和解析后会触发
DOMContentLoaded
事件 - CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
- 在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
- 没有要传输的文件了,断开TCP连接 4 次挥手
性能优化分析
根据上面的过程可以看到,页面的加载过程主要分为下载、解析、渲染三个步骤,整体可以从两个角度来考虑:
- 网页的资源请求与加载阶段
- 网页渲染阶段
网页的资源请求与加载阶段
我们可以打开 Chrome 的调试工具来分析此阶段的性能指标
在建立 TCP 连接的阶段(HTTP 协议是建立在 TCP 协议之上的)
- Queuing 和 Stalled 表示请求队列以及请求等待的时间
- DNS Lookup 表示执行 DNS 查询所用的时间。页面上的每一个新域都需要完整的往返才能执行DNS查询
- Initila connection 和 SSL 包括 TCP 握手重试和协商 SSL 以及 SSL 握手的时间。
在请求响应的阶段
- Request sent 是发出网络请求所用的时间,通常不会超过 1ms
- Watiting(TTFB) 是等待初始响应所用的时间,也称为等待返回首个字节的时间,该时间将捕捉到服务器往返的延迟时间,以及等待服务器传送响应所用的时间。
- Content Download 则是从服务器上接收数据的时间。
资源请求阶段优化方案
依据上面的指标给出以下几点优化方案(仅供参考)
1、划分子域
条件:拥有多个域名
Chrome 浏览器只允许每个源拥有 6 个 TCP 连接,因此可以通过划分子域的方式,将多个资源分布在不同子域上用来减少请求队列的等待时间。然而,划分子域并不是一劳永逸的方式,多个子域意味着更多的 DNS 查询时间。通常划分为 3 到 5 个比较合适。
对如何拆分资源有如下建议:
- 前端类:把项目业务本身的 html、css、js、图标等归为一类
- 静态类:CDN 资源
- 动态类:后端 API
2、DNS 预解析
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP,方法是在 head 标签里写上几个 link 标签
<link rel="dns-prefetch" href="https://www.google.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
对以上几个网站提前解析,这个过程是并行的,不会阻塞页面渲染。
3、预加载
在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。
预加载其实是声明式的 fetch,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载:
<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
4、保持持久连接
HTTP 是一个无状态的面向连接的协议,即每个 HTTP 请求都是独立的。然而无状态并不代表 HTTP 不能保持 TCP 连接,Keep-Alive 正是 HTTP 协议中保持 TCP 连接非常重要的一个属性。 HTTP1.1 协议中,Keep-Alive 默认打开,使得通信双方在完成一次通信后仍然保持一定时长的连接,因此浏览器可以在一个单独的连接上进行多个请求,有效地降低建立 TCP 请求所消耗的时间。
5、CND 加速
使用 CND 加速可以减少客户端到服务器的网络距离。
- CDN 的意图就是尽可能地减少资源在转发、传输、链路抖动等情况下顺利保障信息的连贯性;
- CDN 系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上
- CDN 采用各节点缓存的机制,当我们项目的静态资源修改后,如果 CDN 缓存没有做相应更新,则看到的还是旧的网页,解决的办法是刷新缓存,七牛云、腾讯云都可单独针对某个文件/目录进行刷新;
- CDN 缓存需要合理地使用:图片、常用 js 组件、css 重置样式等,即不常改动的文件可走 CDN,包括项目内的一些介绍页;
还有一种比较流行的做法是让一些项目依赖走 CDN,比如 vuex、vue-router 这些插件通过外链的形式来引入,因为它们都有自己免费的 CDN,这样可以减少打包后的文件体积。
6、设置缓存
缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度。
通常浏览器缓存策略分为两种:强缓存 和 协商缓存。
- 强缓存:实现强缓存可以通过两种响应头实现:
Expires
和Cache-Control
强缓存表示在缓存期间不需要向服务器发送请求 - 协商缓存:缓存过期了就是用协商缓存,其通过
Last-Modified/If-Modified-Since
和ETag/If-None-Match
实现
HTTP 头中与缓存相关的属性,主要有以下几个:
(1) Expires: 指定缓存过期的时间,是一个绝对时间,但受客户端和服务端时钟和时区差异的影响,是 HTTP/1.0 的产物
形如Expires: Wed, 22 Oct 2018 08:41:00 GMT
(2) Cache-Control:比 Expires 策略更详细,max-age 优先级比 Expires 高,其值可以是以下五种情况
- no-cache: 强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送请求到原始服务器(进行过期认证),通常情况下,过期认证需要配合 etag 和 Last-Modified 进行一个比较
- no-store: 告诉客户端不要响应缓存(禁止使用缓存,每一次都重新请求数据)
- public: 缓存响应,并可以在多用户间共享(与中间代理服务器相关)
- private: 缓存响应,但不能在多用户间共享(与中间代理服务器相关)
- max-age: 缓存在指定时间(单位为秒)后过期
(3) Last-Modified / If-Modified-Since: Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将上次从服务器获取的Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
但是如果(服务器)在本地打开缓存文件(或者删了个字符 a 后又填上去),就会造成Last-Modified
被修改,所以在 HTTP / 1.1 出现了ETag
。
(4) Etag / If-None-Match: ETag
类似于文件指纹,If-None-Match
会将当前ETag
发送给服务器,询问该资源ETag
是否变动,有变动的话就将新的资源发送回来。并且ETag
优先级比Last-Modified
高。
由于 etag 要使用少数的字符表示一个不定大小的文件(如 etag: “58c4e2a1-f7”),所以 etag 是有重合的风险的,如果网站的信息特别重要,连很小的概率如百万分之一都不允许,那么就不要使用 etag 了。使用 etag 的代价是增加了服务器的计算负担,特别是当文件比较大时。
选择合适的缓存策略