1、前言
网站的性能一直是前端工程师努力的方向之一,更加流畅的体验,更加快速的页面呈现,都是好的web网站的指标之一。
如今随着网站能包含的元素和内容越来越丰富和多元,在访问网站的时候需要加载的资源变得更多,我们不能再放手不管让网站自由加载所有内容,这样会让用户等待资源加载的时间过长,体验下降。
这次,就以我的个人博客为例子,从最开始的荒芜状态,向业界网站性能标准“秒开”做一次系统性的性能优化。
以下为初始数据,数据来源使用腾讯云RUM性能监控。
2、文件压缩
网站打开的时候需要加载许多的资源,当加载的资源很多很大的时候,我们网站的打开速度就会变慢。在网络中传输大文件是一种非常耗时的行为,所以我们需要将我们的网站资源文件(JS,CSS,图片等)进行压缩,减小它们的体积,这样我们的网站就可以更加快速的下载到本地呈现给用户。
2.1、资源文件的压缩(CSS,JS,html等)
这些文件,我们通常采用gzip
压缩之后再进行传输,这是一个全部浏览器都支持的压缩算法。浏览器接收到gzip方式压缩的资源后,会自动把它解压缩使用。下图可以看到gzip
压缩能带来很大的收益。
如何对这些资源开启gzip
压缩,可以参考我的另一篇文章开启gzip压缩,让你的资源下载更快。
2.2、图片压缩
现在原图的质量越来越好,动辄几mb甚至十几mb,要知道对于网页加载来说,几百kb的网络加载都是昂贵的,所以我们要尽可能减小图片的大小,而减小图片大小的方式就是压缩图片。
高清的图片和经过一定压缩后的图片呈现出来往往肉眼很难分辨他们的质量,所以我们大多情况不用担心压缩导致的图片模糊等情况。但是在尺寸上,压缩却可以带来非常大的收益,这种收益在大图上尤为明显:
- 压缩前
- 压缩后
将所有图片压缩后,我们将会得到一个几乎和原来没有差别的网页呈现,但是现在你的网页需要加载的资源就被大大的减少了。压缩图片的网站推荐 https://tinify.cn/,方便好用,也可以接入API自动化。
2.3、字体文件压缩
一个完整的字体文件(这里指ttf后缀的字体文件)往往非常大,几百kb甚至几mb。一个业界比较常见的字体文件压缩方案是通过字蛛,将字体文件拆分。因为一个完整的字体文件之所以大,是因为它内部包含了这种字体的所有的文字,但是往往网站不需要用到全部的字,所以只把需要的拆出来,这样就大大减少了字体文件的体积。
但是上述的压缩场景对于SPA项目来说不太可能实现,因为字蛛是通过扫描HTML来获取页面需要哪些字的,SPA项目的HTML没有经过加载空空如也,啥也没有;而且对于含有输入场景的网页,由于用户的输入有不确定性,也不能很好的去拆分字体文件,所以这种方案使用场景有限。
我们常用的字体文件格式是TTF(TrueType Font),由苹果和微软为 PostScript 而开发的字体格式,它被开发时就没有考虑为web使用,所以它们没有经过压缩,体积往往较大。
所以我们采用另一种字体文件格式来代替它,那就是WOFF(Web Open Font Format)。该格式完全是为了 Web 而创建,由 Mozilla 基金会、微软和 Opera 软件公司合作推出。WOFF 字体均经过 WOFF 的编码工具压缩,文件大小一般比 TTF 小 40%,加载速度更快,可以更好的嵌入网页中。目前所有的主流浏览器都支持这个更加棒的字体,可以放心使用:
除此之外还有更先进的WOFF2,这是WOFF的下一代,在WOFF原有的基础进一步压缩了30%的大小,不过毕竟是下一代,目前它的支持程度并没有WOFF那般普及:
所以考虑兼容性的情况下,我们对字体的书写往往采用如下的兼容写法,从上到下依次从最新的排到最老的,这样用户代理会从上到下依次尝试加载解析对应的字体,尝试成功后就会停止加载。
@font-face {
font-family: 'Fira Sans';
src: url('./fonts/FiraSans-ExtraBold.woff2') format('woff2'),
url('./fonts/FiraSans-ExtraBold.woff') format('woff'),
url('./fonts/FiraSans-ExtraBold.ttf') format('truetype');
font-weight: 800;
}
3 、异步加载,按需引入
对于我们的网站来说,用户刚进来看到的页面,往往是不需要加载全部资源的,当用户浏览其它页面的时候,才会用到那些资源,自然这些资源我们就可以把他们的加载往后放一放,优先加载首屏需要的资源,这就是分包策略和异步加载。当然复杂的分包策略和异步加载的代码,我们现在基本不用担心,项目一般都是通过webpack配置好即可。
说完异步加载,我们再来说说按需引入。前面说了这么多,其实都是在减小我们网站需要加载的内容体积,那么就有一个非常值得关注的地方,资源利用率。你加载100kb的文件,结果只使用了其中10kb的内容,剩下90kb就被浪费掉了,这些网络资源本可以去加载其它内容,让你的网站更快的呈现,而不是去加载这无效的90kb内容。所以我们在打包项目的时候,要尽可能的减少不必要的内容引入。
用Element-ui举个例子,你的项目应该很少会用到整个UI库的所有组件,所以我们就需要把没有用到的组件的相关代码从最后的产物中剔除来减小我们的产物体积。详细的操作方法可以看官方文档,这里不再赘述,主要是为了说明按需引入的重要性,整个Element-ui打包出来足足有好几百kb的JS文件和一两百kb的CSS文件,这些可能只是因为你用了其中那么2 3个组件,这明显是划不来的,所以按需引入的重要性非常高,引入第三方库的时候因随时注意,是否因为使用一个小功能而大大增加打包后的产物体积。
4、CDN加速
我们的网站资源都需要从服务器上加载,通常我们都把所有的资源放在自己的服务器上,包括HTML和HTML引用的CSS,JS还有图片等。但是我们自己的服务器从各方面来说,都不适合去承载大量的资源请求。所以这个时候就需要用到CDN(Content delivery network)。
内容分发网络(英语:Content Delivery Network或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。 --维基百科
简单的理解就是把你需要加载的资源不是放在你自己的服务器上,而是放在一个托管的服务器上,这个托管的服务器有着更好的性能,更稳定的服务,可以为用户提供更快的访达。这种情况下,我们把HTML放在自己的服务器上,然后把HTML所链接的资源放在CDN上,这样,对于我们自己的服务器来说,就只需承担HTML文档的流量,这是比较小的,然后HTML文档在客户端被解析之后,去对应的CDN上获取各种资源(JS,CSS,图片等)。这样网站所有加载的资源,都能获得一个不错的速度。
5、离线缓存(Service Worker)
5.1、简介
除了上述说的手段,减小资源体积,提高资源传输速度之外,我们还可以有一些优化方式,那就是缓存。我们的资源不总是在更新,所以我们没必要让用户每次访问都重新去拉取一遍资源,我们可以让这些资源缓存在用户本地,等待用户再次访问的时候,可以直接拿出来用。从本地读取肯定是要比网络请求快的。
那么我们如何缓存呢?这里就不讲什么协商缓存和强缓存了,这种网上太多了,不再赘述,这次讲另一种缓存,使用Service Worker。
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。 -- MDN
SW(以下Service Worker都简称SW)是一个比较新的API,它主要是用来解决离线情况下,使用本地缓存的资源来加载web程序。对于SW的介绍、基础用法和常见API,可以参考MDN上的SW的使用教程。本文这里直接从使用说起,如何接入项目进行使用。
SW的API并不简单,从0开始去规划一个项目的本地资源缓存的SW代码是一个相当大的工程,好在Google已经有完善的解决方案,那就是workbox,它提供了很多工具来帮助我们对请求的资源进行管理和缓存。
5.2、项目引入(vue-cli项目例子)
下面使用vue-cli项目进行示范,如何在项目中接入SW和workbox:
// vue.config.js // 首先需要安装 serviceworker-webpack-plugin 插件,它会帮助我们引入SW const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
module.exports = {
chainWebpack: config => {
config.plugin('sw').use(ServiceWorkerWebpackPlugin, [
{
// 你 sw 代码文件所在的位置
entry: path.join(__dirname, 'src/sw.js')
}
])
}
}
首先我们需要安装 serviceworker-webpack-plugin
插件,他可以帮助我们往项目中引入SW文件,因为现在项目都是会经过webpack打包的项目,你不可能像单HTML文件那样,直接相对路径引用SW文件。
// main.js
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
if ('serviceWorker' in navigator) {
runtime.register()
}
然后我们在main.js
中,检测浏览器是否支持SW,如果支持就注册SW,这里直接调用注册函数就好,插件会根据你在config中的配置自动读取对应的文件。
// sw.js
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
// ...
// 图片字体等静态资源缓存
registerRoute(
/.+.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
new CacheFirst({
cacheName: 'assets',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 Days
})
]
})
)
// css js 资源缓存
registerRoute(
/.+.(?:js|css)$/,
new StaleWhileRevalidate({
cacheName: 'resource',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
})
]
})
)
然后就是sw.js,这里就是写SW代码的地方。这里我们需要安装一些workbox
相关的工具,workbox
提供了很多工具,可以根据需要多安装或者少安装,下面几个是我推荐的:
npm i workbox-routing workbox-strategies workbox-expiration workbox-cacheable-response -D
5.3、workbox系列工具使用说明
下面先大致介绍一下刚刚安装的那些包,和里面用到的一些东西。
5.3.1、workbox-routing
这是最重要的工具,是一定要安装的,它负责拦截我们发出去的请求,然后对这些个请求,进行相应的缓存处理。
// registerRoute 是最重要的方法,用来拦截请求,第一个参数可以是正则也可以是一个函数
// 是正则的话,当请求的URL匹配的时候,就会对这个请求执行对应得 CacheHandler
registerRoute(
/.+.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
// 这里暂时用 handler 代替
cacheHandler
)
// 是函数的话,会传入一些请求相关的参数,你可以在这里做一些判断,然后返回Boolean,返回是
// true 的话就适用对应得 CacheHandler
registerRoute(
({ request, url }) => {
return request.destination.length === 0 && url.href.match(/.+/api//)
},
cacheHandler
)
5.3.2、workbox-strategies
这个工具包里面提供了一些预设的缓存处理策略,能够满足我们绝大多数要求
缓存策略介绍
- Stale-While-Revalidate:这个策略的工作路线如下图,它会优先从缓存中读取数据,同时每次请求也会在后台去服务器请求来更新数据。当缓存中没有数据的时候,就会把服务器的请求返回给客户端使用,并且更新缓存。
它非常适合用来缓存一些可变的资源,比如CSS和JS,你可以享受到缓存的速度,即使远端资源更新之后,客户端也只需要刷新下页面就能更新缓存,不用担心读到旧缓存。
- Cache First:这个策略工作路线如下图,它同样优先去读取缓存中的内容,但是如果能读到,就不会发起网络请求,只有在读不到缓存的时候,才会发起网络请求,将请求结果返回客户端并更新缓存。
也就是说这种策略,只要缓存中有对应数据,就不会发起网络请求去更新缓存中的内容,这适合一些不常变化的资源缓存。比如图片,字体资源。
- Network First:这个策略工作路线如下图,它总是优先去发起网络请求,然后把拿到的请求返回给客户端的同时,塞到缓存里。当某次网络请求失败之后,就会从缓存里去读取数据。
从上述我们可以看出,这种策略多是一种应对网络错误时的兜底策略,当发生错误时,我们采取上一次成功的数据返回给用户。这种策略一般用在数据接口的请求上,用来应对网络错误。
- Network Only:这个策略工作路线如下图,它就很简单了,就和它的名字一样,只走网络请求,自我感觉这个策略用处不大,并不清楚应用场景在哪儿。
- Cache Only:这个策略工作路线如下图,和上面
Network Only
同理,就是只走缓存。这个更少用到了,它要求你提前缓存里就有对应的资源才行,需要配合workbox
的另一个功能precache
使用,这里不做展开。
如何使用
// css js 资源缓存
// 这里用缓存 css js举例
registerRoute(
/.+.(?:js|css)$/,
new StaleWhileRevalidate({
cacheName: 'resource',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
})
]
})
)
这里我们用缓存css 和 js举例,通过上面一节的说明,对应这种远端可能会更新的资源,建议采用第一种Stale-While-Revalidate
的缓存策略。使用方法就是把 workbox-routing
那一节例子中的 CacheHandler
占位,改成对应策略的实例即可。
在实例的时候还可以填入一些 options,比如cacheName和plugins,cacheName后面会反映到本地cache Api中存储的地方,plugins可以提供一些额外的功能,比如这里,我们希望每次缓存的资源都是200请求的,而不是404或者500状态码的资源。所以从 workbox-cacheable-response
中导入一个插件,他可以帮我们过滤相应的状态码。
5.3.3、workbox-expiration
这里面提供了一些供缓存策略实例使用的插件(就如上一节里提到的,缓存策略实例化的时候,传入的options里可以有plugins选项),使用例子如下:
import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
// 图片字体等静态资源缓存
registerRoute(
/.+.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
new CacheFirst({
cacheName: 'assets',
plugins: [
new ExpirationPlugin({
// 最多缓存 60 个项目
maxEntries: 60,
// 缓存时间最长 30 天
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 Days
})
]
})
)
我们在CacheFirst
策略中,实例化ExpirationPlugin
插件,这个插件可以帮助我们限制缓存项目上限和缓存的时间。因为CacheFirst
策略在有缓存的时候会始终读取缓存,虽然这里存的是不常变化的内容,但是我们仍然不希望无限的增加缓存的内容并且无限期的保留缓存,这个插件就可以很好的帮我们实现这些功能。
5.3.4、workbox-cacheable-response
这个里面提供的插件的使用已经在 5.3.2 那一节提到,就是帮助我们根据状态码,选择性的将网络请求的资源包塞到缓存里。
5.4、Service Worker小结
SW这个东西呢,我觉得属于是缓存方面功能的天花板,它可以灵活的定义缓存的项目,缓存的时机等,也可以操作缓存的内容而达到一些其他的目的。但是毕竟是一个比较新的功能,兼容性上的障碍使它只是在缓存这块处于一个锦上添花的位置,不能全盘依靠SW来做缓存。
6、结尾
经过好几天的优化,在没有上终极首屏优化方案(SSR)之前,网站已经达到了一个还不错的速度:
这里的数据都只是取得首页的数据,不同页面打开的速度可能会有些差别,这里只取我认为最重要的首页的打开数据作为前后对比。后续还可以使用SSR方案进行进一步的优化,不过这个方案需要项目整体重构,一时半会儿搞不出来。
参考文献
Web 字体简介: TTF, OTF, WOFF, EOT & SVG
按需引入element-ui
Vue 3, PWA & service worker
Workbox get started