欢迎来到「前端性能优化之旅」的第一站 —— 缓存。git
当浏览器想要获取远程的数据时,咱们的性能之旅就开始了。然而,咱们并不会当即动身(发送请求)。在计算机领域,不少性能问题都会经过增长缓存来解决,前端也不例外。和许多后端服务同样,前端缓存也是多级的。下面让咱们一块儿来具体看一看。github
经过结合本地存储,能够在业务代码侧实现缓存。web
对于一些请求,咱们能够直接在业务代码侧进行缓存处理。缓存方式包括 localStorage
、sessionStorage
、indexedDB
。把这块加入缓存的讨论也许会有争议,但利用好它确实能在程序侧达到一些相似缓存的能力。后端
例如,咱们的页面上有一个日更新的榜单,咱们能够作一个当日缓存:浏览器
// 当用户加载站点中的榜单组件时,能够经过该方法获取榜单数据
async function readListData() {
const info = JSON.parse(localStorage.getItem('listInfo'));
if (isExpired(info.time, +(new Date))) {
const list = await fetchList();
localStorage.setItem('listInfo', JSON.stringify({
time: +(new Date),
list: list
}));
return list;
}
return info.list;
}
复制代码
localStorage
你们都比较了解了,indexedDB
可能会了解的更少一些。想快速了解 indexedDB
使用方式能够看这篇文章[1]。缓存
从前端视角看,这是一种本地存储;但若是从整个系统的维度来看,不少时候其实也是缓存链条中的一环。对于一些特殊的、轻量级的业务数据,能够考虑使用本地存储做为缓存。性能优化
当你访问一个页面及其子资源时,有时候会出现一个资源被使用屡次,例如图标。因为该资源已经存储在内存中,再去请求反而画蛇添足,浏览器内存则是最近、最快的响应场所。服务器
内存缓存并没有明确的标准规定,它与 HTTP 语义下的缓存关联性不大,算是浏览器帮咱们实现的优化,不少时候其实咱们意识不到。网络
对内存缓存感兴趣,能够在这篇文章[2]的 Memory Cache 部分进一步了解。
当咱们没有命中内存缓存时,是否就开始发送请求了呢?其实不必定。
在这时咱们还可能会碰到 Cache API 里的缓存,提到它就不得不提一下 Service Worker 了。它们一般都是配合使用的。
首先明确一下,这层的缓存没有规定说该缓存什么、什么状况下须要缓存,它只是提供给了客户端构建请求缓存机制的能力。若是你对 PWA 或者 Service Worker 很了解,应该很是清楚是怎么一回事。若是不了解也没有关系,咱们能够简单看一下:
首先,Service Worker 是一个后台运行的独立线程,能够在代码中启用
// index.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(function () {
// 注册成功
});
}
复制代码
以后须要处理一些 Service Worker 的生命周期事件,而其中与这里提到的缓存功能直接相关的则是请求拦截:
// sw.js
self.addEventListener('fetch', function (e) {
// 若是有cache则直接返回,不然经过fetch请求
e.respondWith(
caches.match(e.request).then(function (cache) {
return cache || fetch(e.request);
}).catch(function (err) {
console.log(err);
return fetch(e.request);
})
);
});
复制代码
以上代码会拦截全部的网络请求,查看是否有缓存的请求内容,若是有则返回缓存,不然会继续发送请求。与内存缓存不一样,Cache API 提供的缓存能够认为是“永久性”的,关闭浏览器或离开页面以后,下次再访问仍然可使用。
Service Worker 与 Cache API 实际上是一个功能很是强大的组合,可以实现堆业务的透明,在兼容性上也能够作成渐进支持。仍是很是推荐在业务中尝试的。固然上面代码简略了不少,想要进一步了解 Service Worker 和 Cache API 的使用能够看这篇文章[3]。同时推荐使用 Google 的 Workbox。
若是 Service Worker 中也没有缓存的请求信息,那么就会真正到 HTTP request 的阶段了。这个时候出现的就是咱们所熟知的 HTTP 缓存规范。
HTTP 有一系列的规范来规定哪些状况下须要缓存请求信息、缓存多久,而哪些状况下不能进行信息的缓存。咱们能够经过相关的 HTTP 请求头来实现缓存。
HTTP 缓存大体能够分为强缓存与协商缓存。
在强缓存的状况下,浏览器不会向服务器发送请求,而是直接从本地缓存中读取内容,这个“本地”通常就是来源于硬盘。这也就是咱们在 Chrome DevTools 上常常看到的「disk cache」。
与其相关的响应头则是 Expires
和 Cache-Control
。在 Expires
上能够设置一个过时时间,浏览器经过将其与当前本地时间对比,判断资源是否过时,未过时则直接从本地取便可。而 Cache-Control
则能够经过给它设置一个 max-age
,来控制过时时间。例如,max-age=300
就是表示在响应成功后 300 秒内,资源请求会走强缓存。
你可能也感受到了,强缓存不是那么灵活。若是我在 300 秒内更新了资源,须要怎么通知客户端呢?经常使用的方式就是经过协商缓存。
咱们知道,远程请求慢的一大缘由就是报文体积较大。协商缓存就是但愿能经过先“问一问”服务器资源到底有没有过时,来避免无谓的资源下载。这伴随的每每会是 HTTP 请求中的 304 响应码。下面简单介绍一下实现协商缓存的两种方式:
一种协防缓存的方式是:服务器第一次响应时返回 Last-Modified
,而浏览器在后续请求时带上其值做为 If-Modified-Since
,至关于问服务端:XX 时间点以后,这个资源更新了么?服务器根据实际状况回答便可:更新了(状态码 200)或没更新(状态码 304)。
上面是经过时间来判断是否更新,若是更新时间间隔太短,例如 1s 一下,那么使用更新时间的方式精度就不够了。因此还有一种是经过标识 —— ETag
。服务器第一次响应时返回 ETag
,而浏览器在后续请求时带上其值做为 If-None-Match
。通常会用文件的 MD5 做为 ETag
。
做为前端工程师,必定要善于应用 HTTP 缓存。若是想要了解更多关于 HTTP 缓存的内容,能够阅读这篇文章[4]。
上面这些的各级缓存的匹配机制里,都是包含资源的 uri 的匹配,即 uri 更改后不会命中缓存。也正是如此,咱们目前在前端实践中都会把文件 HASH 加入到文件名中,避免同名文件命中缓存的旧资源。
假如很不幸,以上这些缓存你都没有命中,那么你将会碰到最后一个缓存检查 —— Push Cache。
Push Cache 实际上是 HTTP/2 的 Push 功能所带来的。简言之,过去一个 HTTP 的请求链接只能传输一个资源,而如今你在请求一个资源的同时,服务端能够为你“推送”一些其余资源 —— 你可能在在不久的未来就会用到一些资源。例如,你在请求 www.sample.com 时,服务端不只发送了页面文档,还一块儿推送了 关键 CSS 样式表。这也就避免了浏览器收到响应、解析到相应位置时才会请求所带来的延后。
不过 HTTP/2 Push Cache 是一个比较底层的网络特性,与其余的缓存有不少不一样,例如:
若是对 HTTP/2 Push 感兴趣,能够看看这篇文章[5]。
好了,到目前为止,咱们可能尚未发出一个真正的请求。这也意味着,在缓存检查阶段咱们就会有不少机会将后续的性能问题扼杀在摇篮之中 —— 若是远程请求都没必要发出,又何必优化加载性能呢?
因此,审视一下咱们的应用、业务,看看哪些性能问题是能够在源头上解决的。
不过不少时候,能经过缓存解决的问题只有一部分。因此下面咱们会继续这趟旅行,目前咱们已经有了一个好的开始,不是么?
目前内容已所有更新至 ✨ fe-performance-journey ✨ 仓库中,陆续会将内容同步到掘金上。若是但愿尽快阅读相关内容,能够直接去该仓库中浏览文章。
喜欢的朋友能够 star 一下,后续也会继续更新更多性能优化相关的内容。