如何让网站加载速度变快

从刚学前端开始就开始折腾博客,一直在尝试如何让博客访问变快,本文总结了一些从前端的角度让站点打开速度变快的方案。

<!--more-->

这里的网站加载速度,指的是从输入地址栏,到页面完整展示整个页面的过程,包括加载HTML文档、加载页面静态资源等过程。参考

1. 提前请求

可以把一些资源提前准备好,当访问页面时就可以加快加载速度。

1.1. DNS预解析

DNS查询也是需要消耗网络资源的,可以通过DNS预解析来优化页面的加载速度,X-DNS-Prefetch-Control头控制着浏览器的 DNS 预读取功能。

DNS 预读取是一项使浏览器主动去执行域名解析的功能,其范围包括文档的所有链接,无论是图片的,CSS 的,还是 JavaScript 等其他用户能够点击的 URL。

因为预读取会在后台执行,所以 DNS 很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟,详情参考DNS预读取-MDN。预解析的实现有如下方式:

用meta信息来告知浏览器, 当前页面要做DNS预解析:

<meta http-equiv="x-dns-prefetch-control" content="on" />

在页面header中使用link标签来强制对DNS预解析:

<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />

Chrome内置了DNS Prefetching技术,Firefox 3.5 也引入了这一特性,由于Chrome和Firefox 3.5本身对DNS预解析做了相应优化设置,所以设置DNS预解析的不良影响之一就是可能会降低使用上述浏览器的用户体验。

1.2. preload

由于script标签加载同步脚本文件会阻塞文档的解析,可以通过link标签的preload标签预加载脚本文件,并将文件内容保存在内容中,但是不会执行,只有当遇到script标签加载的也是preload相同的脚本资源时,才会执行预加载的脚本

<!-- 预加载 -->
<link rel="preload" href="/index.js" as="script">
<!-- 遇见script标签时不会重新下载,而是执行前面preload的文件 -->
<script src="/index.js"></script>

1.3. prefetch

在浏览器空闲的时候,现在对应资源,并缓存到磁盘上,当有页面使用该资源时,直接从磁盘缓存读取。

<link href="/index.js" rel="prefetch">

需要注意的是,如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,就会重复再次发起请求,因此不要再马上需要使用资源的页面上使用prefetch,此时应该换用preload

2. 加快请求资源

在当前页面的加载中,我们需要尽可能保证资源加载的速度。

2.1. 浏览器最大请求域名限制

参考:

浏览器对同一个服务器的并发连接个数都是有限制,如果请求的文件数量过多,浏览器只会先下载最大并发数的文件,后续文件需要等待前面文件下载完毕后才会继续请求,这就导致文件下载完毕的总体时间增加。下面是不同浏览器对于同一域名请求并发数目限制,图片来源

常见的处理方式是:通过多个域名增加浏览器对来自同一网页的文件请求并发数。一般来讲,在线上项目中,静态资源都会使用单独的域名来进行加载

  • 静态内容和动态内容分服务器存放,使用不同的服务器处理请求。数据服务器处理动态内容,CDN服务器提供静态资源,这样各司其职,且使得CDN缓存更方便

  • 突破浏览器并发限制,浏览器同一时间可以从一个域名下载资源的数目有限制,这种技术被称为domain hash

  • 独立的域名不会携带Cookie等用户身份信息,减少了请求头的大小,可以节省带宽,这种技术被称为``cookie free`

但是过多的域名会增加DNS解析耗时问题,可以通过前面的DNS预解析来减缓这个问题

2.2. 减少请求数量

http协议是无状态的应用层协议,意味着每次http请求都需要建立通信链路、进行数据传输,而在服务器端,每个http都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少http请求的数目可有效提高访问性能。

减少http请求数量的主要手段是合并CSS、JavaScript、图片等静态资源。

  • 通过webpack等打包工具,将Javascript和CSS进行合并
  • 将图片合并成雪碧图或者直接转换成base64内联,可以控制小图标的请求数量和体积
  • 按需加载资源,图片懒加载,优先加载首屏资源等方式
  • 使用字体图标替代图片小图标

此外,恰当的缓存设置可以大大的减少 HTTP请求,因为被浏览器缓存的资源在有效期内不会重新发送请求。

合并文件带来的一个问题是:如果是单个一个模块改动,也会导致整个合并文件并重新下载,对于缓存来说是很不利的,因为其他未更改的模块内部完全是不需要更新下载的。因此,目前的打包策略是:将不经常修改的库文件打包到一个文件如vendor.js,而经常变化的业务代码打包到一个文件如index.js

2.3. 压缩文件体积

在服务器端对文件进行压缩,在浏览器端对文件解压缩,可有效减少通信传输的数据量,加快文件传输速度。在目前的项目中,一般会使用下面

  • 使用UglifyJS压缩JS文件,
  • 使用PostCSS压缩CSS文件
  • 使用imagemin来压缩图片,这里推荐一个超级好用的图片压缩工具TinyPNG

压缩代码文件还可以达到混淆代码的目的。一套比较完善的前端开发环境下,一般提供了内置的工具来实现上述需求。

此外,由于文本文件的压缩效率可达到80%以上,因此HTML、CSS、javascript等静态资源文件启用GZip压缩可达到较好的效果。由于GZip压缩对服务器和浏览器产生一定的压力,在通信带宽良好,而服务器资源不足的情况下要权衡考虑。

2.4. HTTP2带来的影响

参考:浅析HTTP/2的多路复用

HTTP2提供了一些新的特性,如Keep-Alive、多路复用等,基于这些特性,我们的一些优化措施可能就不再是必要的了

Keep-Alive可以实现:一定时间内,同一域名多次请求数据,只建立一次HTTP请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题,此时我们也就不再需要合并静态资源文件。

多路复用基于新增的二进制分帧层。二进制分帧层将数据转换成二进制,也就是说HTTP/2中所有的内容都是采用二进制传输。

* 帧是HTTP2中数据传输的最小单位;
* 每个帧都有stream_ID字段,表示这个帧属于哪个流
* 接收方把stream_ID相同的所有帧组合到一起就是被传输的内容了。

在这种传输模式下,HTTP请求变得十分廉价,我们不需要再时刻顾虑网站的http请求数是否太多、TCP连接数是否太多、是否会产生阻塞等问题了,同样地,我们也就不再需要为静态资源分配多个域名,节省了DNS开销。

由于目前并非所有服务器和浏览器都支持HTTP2协议,因此这里主要是做一点扩展,前面提到的文件合并、并发限制还是有必要在项目中进行优化的。

3. 使用浏览器缓存

静态资源本身具有访问频率高、承接流量大的特点,因此静态资源加载速度始终是前端性能的一个非常关键的指标。

由于静态资源的更新频率很低,如果将这些文件缓存在浏览器中,可以极大程度低减少文件请求数量,加快资源加载。

浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  • Memory Cache,放在内存中的资源缓存,如base64图片,体积较小的JS和CSS代码
  • Service Worker Cache,Service Worker 是一种独立于主线程之外的 Javascript 线程,可以帮我们实现离线缓存、消息推送和网络代理等功能
  • HTTP Cache,分为强缓存和协商缓存,是我们需要着重处理的缓存
  • Push Cache,是指 HTTP2 在 server push 阶段存在的缓存,这种一种会在与会话阶段的缓存,session终止时缓存就会被释放

3.1. Service Worker Cache

参考:

Service worker是一个注册在指定源和路径下的事件驱动worker,它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。

这里有一个官方的demo,展示了使用Service Worker来控制缓存,其大致实现思路是

  • 在worker中监听fetch事件,判断请求资源是否匹配cache.match
  • 如果命中worker缓存,则直接返回;如果未命中,则继续发送请求到服务器
  • 获取响应结果,catch.put缓存到本地,然后返回响应,下次请求时则可以直接返回缓存

这种方式提供了让前端控制请求和响应的API,可以实现颗粒化的缓存。

3.2. HTTP Cache

参考:前端必须要懂的浏览器缓存机制](https://juejin.im/entry/59c8d4675188256bb018ff89)

当客户端请求某个资源时,缓存的工作流程如下:

  • 首先判断请求资源是否缓存在本地,如果存在,则通过max-age等字段进行新鲜度校验
    • 校验通过,直接返回本地缓存文件200(from cache)
    • 校验不通过,则向服务器发送再验证请求
  • 服务器接收到再验证请求,检测文件是否通过再验证
    • 再验证通过,文件未更新,返回304,浏览器更新本地文件新鲜度,返回缓存文件304(not modified)
    • 再验证未通过,检测资源是否存在
      • 不存在,返回404,浏览器删除本地缓存,返回404(not found)
      • 存在,返回新资源,浏览器将新资源缓存到本地,返回200
  • 当首次请求新资源时,同上述检测资源存在后的处理流程

根据上面流程,我们可以看见两处检测,针对本地缓存文件的新鲜度校验和服务器的再验证,这两步也可以看做是浏览器的强缓存和协商缓存策略。

强缓存

新鲜度校验的主要作用是浏览器自己检测本地缓存文件是否已经失效,与下面几个字段有关

  • Expires,该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间。由于很多服务器跟客户端存在时钟不一致的情况,因此该字段代表的过期时间并不是十分准确
  • Cache-Control:max-age,该字段是 http1.1 的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒
  • 如果如果同时设置了Cache-ControlExpiresCache-Control会覆盖Expires

协商缓存

再验证主要用于浏览器向服务器询问本地缓存是否已经失效,有两种方式。

一种是询问文件的最后修改

  • Last-Modified,值为资源最后更新时间,单位精确到秒,随服务器response返回
  • If-Modified-Since,通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存

另外一种是询问文件的内容是否发生变化

  • ETag,表示资源内容的唯一标识,随服务器response返回
  • If-None-Match,服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存

从字面意义可以看出,文件内容的变换比最后修改时间的变化更具有参考性,因此If-None-Match的优先级要大于If-modified-Since

如果请求报文中同时包含这两个首部,服务器会优先验证资源标识符而不是资源修改时间,则只有当他们都满足时,服务器才返回304 NOT Modified

3.3. 缓存更新策略

当然静态资源文件变化后,需要及时应用到浏览器,在版本更新后,修改模板内引用的资源名文件名,这样就相当于第一次访问新的资源,从而直接更新文件。在前端单页应用中,可以通过webpack-html-plugin等工具自动更新打包资源文件名,从而绕开缓存直接更新文件。

在这种更新策略下,

  • HTML文件会使用协商缓存,每次访问时都校验新鲜度,避免HTML缓存导致用户无法及时更新到新版本
  • CSS、JS、图片等静态资源文件使用强缓存,设置较长的过期时间,并通过文件hash区分旧版资源

另外使用浏览器缓存策略的网站在更新静态资源时,应采用逐量更新的方法,比如需要更新10个图标文件,不宜把10个文件一次全部更新,而是应该一个文件一个文件逐步更新,并有一定的间隔时间,以免用户浏览器忽然大量缓存失效,集中更新缓存,造成服务器负载骤增、网络堵塞的情况。

使用不同方式刷新浏览器,也会触发不同的缓存校验机制

  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存;

4. 使用CDN

CDN是静态资源提速的重要手段。

4.1. 基本原理

参考

CDN网络是在用户和服务器之间增加Cache层,如何将用户的请求引导到Cache上获得源服务器的数据,主要是通过接管DNS实现

当业务需要接入到CDN时,用户只需调整自己的DNS配置信息,从直接指向自己的源服务器,修改为CNAME类型记录,将域名指向CDN厂商所提供的接入域名即可。

未使用CDN,请求资源时解析域名过程如下

  • 用户向浏览器提供要访问的域名,浏览器通过调用DNS解析服务,获得该域名对应的IP
  • 浏览器使用该IP,建立连接并发送请求,最后根据服务器返回的数据显示网页内容

使用了CDN,请求资源时解析域名过程如下

  • 用户向浏览器提供要访问的域名,浏览器通过调用DNS解析服务,获取到的是该域名对应的CNAME记录
  • 由于CNAME实际还是域名,浏览器再次对获得的CNAME域名进行解析,以得到CND服务器的IP地址,在此过程中,会根据地理位置信息等解析返回距离用户最近的IP地址
  • 浏览器在得到最近CDN服务器的IP地址以后,发出请求,并根据服务器返回的数据显示网页内容
  • 此外,CND缓存服务器还会根据浏览器访问的原始域名,通过Cache内部专用DNS解析得到此域名原始服务器的实际IP地址,再由缓存服务器向此实际IP地址提交访问请求,判断是否需要更新CND服务器上的缓存数据

4.2. 配置CNAME

下面是配置七牛cdn加速的操作流程,通过配置可以体会到CNAME的工作原理

  • 首先去七牛管理后台添加一个域名,如cdn.shymean.com,后续内容管理里面的资源都可以通过该域名进行访问。
  • 添加完毕后,可以得到一个CNAMEcdn.shymean.com.qiniudns.com,复制该CNAME,然后去域名管理后台(我的域名是阿里云万网购买的),新增域名解析,将cdn.shymean.com选择CNAME解析方式,解析到cdn.shymean.com.qiniudns.com
  • 然后就大功告成了。

可以看见,CNMAE实际上就是CDN服务商提供的一个域名, 由于静态资源往往不需要用户信息如Cookie等,因此可以使用独立的cdn域名来访问。

5. 总结

本文总结了前端性能优化中一个比较重要的场景:让网站加载速度变快

  • 提前准备请求需要的DNS、静态文件等资源
  • 加快资源访问速度,如绕开浏览器统一域名并发数量限制、合并请求文件、压缩体积等
  • 了解浏览器缓存原理,通过缓存进一步减少文件请求数量
  • 了解CDN使用及相关原理,配置CDN加快静态资源的访问

此外,还整理了在HTTP2为当前某些优化手段带来的一些影响。前端性能优化除了页面访问速度之外,还有其他如交互性能流畅等方面的优化,后续会继续整理。