前端性能优化(存储篇)
缓存
浏览器缓存机制四个方面,优先级依次排列如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
强缓存
-
强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
-
命中强缓存的情况下,返回的 HTTP 状态码为 200
- expires(expires: Wed, 11 Sep 2019 16:12:18 GMT)
- expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。
- 缺陷:对“本地时间”的依赖。如果服务端和客户端的时间设置不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。
- Cache-Control (cache-control: max-age=31536000)
-
HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务,可以视作是 expires 的完全替代方案。
-
Cache-Control 的优先级高于 expires。
max-age 与 s-maxage
-
max-age=设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
-
s-maxage=覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效
-
s-maxage仅在代理服务器中生效,客户端中我们只考虑max-age。
// cache-control: max-age=3600, s-maxage=31536000
no-store与no-cache
-
no-cache 绕开了浏览器,为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期。(no-cache 可以在本地缓存,可以在代理服务器缓存,但是这个缓存要服务器验证才可以使用 )
-
no-store 就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。
public 与 private
-
为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果设置了 private,则该资源只能被浏览器缓存。private 为默认值。
-
但多数情况下,public 并不需要我们手动设置(max-age有的话,表示响应是可以缓存的)
协商缓存
-
浏览器与服务器合作之下的缓存策略。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
-
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304
Last-Modified 与 Etag
- Last-Modified 是一个时间戳,如果启用了协商缓存,它会在首次请求时随着 Response Headers 返回:
// Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
// 后面的每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
// If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
-
服务器接收到这个时间戳(If-Modified-Since)后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化
-
缺点:(服务器并没有正确感知文件的变化)
-
编辑了文件,但没有改变文件内容。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。
-
修改文件的速度过快。由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。
- Etag(由服务器为每个资源生成的唯一的标识字符串),作为 Last-Modified 的补充。首次请求时,我们会在响应头里获取到一个最初的标识符字符串。
// ETag: W/"2a3b-1602480f459"
// 下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:
// If-None-Match: W/"2a3b-1602480f459"
-
缺点: Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能。
-
Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
MemoryCache
-
MemoryCache,是指存在内存中的缓存。优先级上是浏览器最先尝试去命中的一种缓存。效率上是响应速度最快的一种缓存。但也是“短命”的。它和渲染进程“生死相依”,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。
-
Base64格式的图片几乎永远可以被塞进 memory cache,另外体积不大的 JS、CSS 文件有较大地被写入内存。
Service Worker Cache
-
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。
-
可以帮我们实现离线缓存、消息推送和网络代理等功能
-
Service Worker 的生命周期包括 install、active、working 三个阶段。一旦 Service Worker 被 install,它将始终存在,只会在 active 与 working 之间切换,除非我们主动终止它。这是它可以用来实现离线存储的重要先决条件。
window.navigator.serviceWorker.register('/vfrank.js').then(
function() {
console.log('注册成功')
}).catch(err => {
console.error("注册失败")
})
- 在 vfrank.js 中,我们进行缓存的处理。假设我们需要缓存的文件分别是 vfrank.html,vfrank.css 和 vfrank.js:
// Service Worker会监听 install事件,我们在其对应的回调里可以实现初始化的逻辑
self.addEventListener('install', event => {
event.waitUntil(
// 考虑到缓存也需要更新,open内传入的参数为缓存的版本号
caches.open('vfrank-v1').then(cache => {
return cache.addAll([
// 此处传入指定的需缓存的文件名
'/vfrank.html',
'/vfrank.css',
'/vfrank.js'
])
})
)
})
// Service Worker会监听所有的网络请求,网络请求的产生触发的是fetch事件,我们可以在其对应的监听函数中实现对请求的拦截,进而判断是否有对应到该请求的缓存,实现从Service Worker中取到缓存的目的
self.addEventListener('fetch', event => {
event.respondWith(
// 尝试匹配该请求对应的缓存值
caches.match(event.request).then(res => {
// 如果匹配到了,调用Server Worker缓存
if (res) {
return res;
}
// 如果没匹配到,向服务端发起这个资源请求
return fetch(event.request).then(response => {
if (!response || response.status !== 200) {
return response;
}
// 请求成功的话,将请求缓存起来。
caches.open('vfrank-v1').then(function(cache) {
cache.put(event.request, response);
});
return response.clone();
});
})
);
});
Push Cache
-
是指 HTTP2 在 server push 阶段存在的缓存
-
Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
-
Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
-
不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
本地存储
Cookie (Cookie 的详细内容,可以在 Chrome 的 Application 面板中查看)
-
Cookie 是有体积上限最大只能有 4KB。当 Cookie 超过 4KB 时,它将被裁切。
-
通过响应头里的 Set-Cookie 指定要存储的 Cookie 值
// Set-Cookie: name=xiuyan; domain=xiuyan.me
Web Storage
-
Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制。它又分为 Local Storage 与 Session Storage。
-
生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。
-
作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。
// 存储数据:setItem()
localStorage.setItem('user_name', 'xiuyan')
// 读取数据: getItem()
localStorage.getItem('user_name')
// 删除某一键名对应的数据: removeItem()
localStorage.removeItem('user_name')
// 清空数据记录:clear()
localStorage.clear()
IndexDB
- IndexDB 是一个运行在浏览器上的非关系型数据库
- 打开/创建一个 IndexDB 数据库(当该数据库不存在时,open 方法会直接创建一个名为 xiaoceDB 新数据库)。
// 后面的回调中,我们可以通过event.target.result拿到数据库实例
let db
// 参数1位数据库名,参数2为版本号
const request = window.indexedDB.open("xiaoceDB", 1)
// 使用IndexDB失败时的监听函数
request.onerror = function(event) {
console.log('无法使用IndexDB')
}
// 成功
request.onsuccess = function(event) {
// 此处就可以获取到db实例
db = event.target.result
console.log("你打开了IndexDB")
}
- 创建一个 object store(object store 对标到数据库中的“表”单位)。
// onupgradeneeded事件会在初始化数据库/版本发生更新时被调用,我们在它的监听函数中创建object store
request.onupgradeneeded = function(event) {
let objectStore
// 如果同名表未被创建过,则新建test表
if (!db.objectStoreNames.contains('test')) {
objectStore = db.createObjectStore('test', { keyPath: 'id' })
}
}
- 构建一个事务来执行一些数据库操作,像增加或提取数据等。
// 创建事务,指定表格名称和读写权限
const transaction = db.transaction(["test"], "readwrite")
// 拿到Object Store对象
const objectStore = transaction.objectStore("test")
// 向表格写入数据
objectStore.add({ id: 1, name: 'xiuyan' })
- 通过监听正确类型的事件以等待操作完成。
// 操作成功时的监听函数
transaction.oncomplete = function(event) {
console.log("操作成功")
}
// 操作失败时的监听函数
transaction.onerror = function(event) {
console.log("这里有一个Error")
}