让自己网站的加载速度变快的几种方案

本文总结了一些从前端的角度让站点打开速度变快的方案。这里的网站加载速度,指的是从输入地址栏,到页面完整展示整个页面的过程,包括加载 HTML 文档、加载页面静态资源等过程。

提前请求

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

DNS 预解析

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

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

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

用 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 预解析的不良影响之一就是可能会降低使用上述浏览器的用户体验。

preload

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

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

 

prefetch

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

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

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

加快请求资源

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

浏览器最大请求域名限制

参考:

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

常见的处理方式是:通过多个域名增加浏览器对来自同一网页的文件请求并发数。

一般来讲,在线上项目中,静态资源都会使用单独的域名来进行加载:

  • 静态内容和动态内容分服务器存放,使用不同的服务器处理请求。数据服务器处理动态内容,CDN 服务器提供静态资源,这样各司其职,且使得 CDN 缓存更方便;
  • 突破浏览器并发限制,浏览器同一时间可以从一个域名下载资源的数目有限制,这种技术被称为 domain hash;
  • 独立的域名不会携带 Cookie 等用户身份信息,减少了请求头的大小,可以节省带宽,这种技术被称为“cookie free`;

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

减少请求数量

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

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

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

此外,恰当的缓存设置可以大大的减少 HTTP 请求,因为被浏览器缓存的资源在有效期内不会重新发送请求。
合并文件带来的一个问题是:如果是单个一个模块改动,也会导致整个合并文件并重新下载,对于缓存来说是很不利的,因为其他未更改的模块内部完全是不需要更新下载的。因此,目前的打包策略是:将不经常修改的库文件打包到一个文件如 vendor.js,而经常变化的业务代码打包到一个文件如 index.js。

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

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

压缩代码文件还可以达到混淆代码的目的。一套比较完善的前端开发环境下,一般提供了内置的工具来实现上述需求。
此外,由于文本文件的压缩效率可达到 80%以上,因此 HTML、CSS、javascript 等静态资源文件启用 GZip 压缩可达到较好的效果。由于 GZip 压缩对服务器和浏览器产生一定的压力,在通信带宽良好,而服务器资源不足的情况下要权衡考虑。

HTTP2 带来的影响
参考:浅析 HTTP/2 的多路复用[7]。
HTTP2 提供了一些新的特性,如 Keep-Alive、多路复用等,基于这些特性,我们的一些优化措施可能就不再是必要的了
Keep-Alive 可以实现:一定时间内,同一域名多次请求数据,只建立一次 HTTP 请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题,此时我们也就不再需要合并静态资源文件。

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

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

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

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

使用浏览器缓存

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

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

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

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

 

Service Worker Cache

参考:
Cache – MDN[8]
Service Worker API[9]

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

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

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

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

HTTP Cache

参考:前端必须要懂的浏览器缓存机制[12]

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

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

当首次请求新资源时,同上述检测资源存在后的处理流程根据上面流程,我们可以看见两处检测,针对本地缓存文件的新鲜度校验和服务器的再验证,这两步也可以看做是浏览器的强缓存和协商缓存策略。

强缓存

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

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

缓存更新策略

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

在这种更新策略下,

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

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

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

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

使用 CDN

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

基本原理
参考

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 服务器上的缓存数据

配置 CNAME

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

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

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

总结

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

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

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

 

参考


来源:https://juejin.cn/post/6844904078447755271

© 版权声明

☆ END ☆
喜欢就点个赞吧
点赞0 分享
图片正在生成中,请稍后...