ShiningDan的博客

性能优化之资源缓存与更新

在上一节性能优化之静态资源优化中,我们对博客中使用到的 CSS、JavaScript、图片等做了比如大小、合并、懒加载等优化,在这一节中,我们将使用浏览器的缓存能力,对这些资源进行进一步的优化,来提升首屏加载速度等,并且我们还将讨论,针对缓存在浏览器中的数据,如何进行缓存的更新。

使用 LocalStorage 进行本地存储

关于 LocalStorage 的介绍可以查看 Window.localStorage | MDN使用 Web Storage API,我这里就不再赘述了。

首先,我们需要进行本地存储的项目有,CSS 文件和 JS 文件,特别是 CSS 文件中添加了 Data URI 以后,文件的大小会增加很多,这种文件可以考虑使用 LocalStorage 进行缓存,也有一些全局的样式,全局的 JS 文件,或者一些不经常改变的文件或样式都可以考虑使用 LocalStorage 进行缓存,下面,我们将展示的是对 CSS 文件的缓存。

函数的定义

首先定义 LocalStorage Save(ls) 函数和 LocalStorage Load(ll) 函数,用来保存文件内容到 LocalStorage 里面和从 LocalStorage 读取内容,添加到 <head> 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function ls(name) {
let target = document.getElementById(name);
if (!target) {
throw new Error('ls save fn not find ' + name);
} else {
if (window.localStorage) {
try {
window.localStorage.setItem(name, target.innerHTML);
document.cookie = 'v=0;'
} catch(e) {
console.log(e);
}
}
}
}
function ll(name, tag) {
let storage = window.localStorage.getItem(name);
if (!storage) {
// 如果 cookie 中存在,但是在 localstorage 中找不到需要如何处理?先删除 cookie,然后刷新页面。
document.cookie = 'v=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
window.location.reload();
throw new Error('ls load fn not find ' + name);
} else {
let type = tag ? 'script' : 'style'; // 设置 0 来添加 style,设置 1 来添加 script
let elem = document.createElement(type);
elem.innerHTML = storage;
document.head.appendChild(elem);
}
}

除了实现添加功能以外,还需要使用 cookie 来标记用户。如果用户已经下载 common_css,则下次就不用在 HTML 页面中添加 common_css 的部分,而是转为调用 ll('common_css', 0);。这一部分的内容通过 Express 中间件来实现:

1
2
3
4
5
6
7
8
9
exports.checkll = function(req, res, next) {
if (req.cookies.v) {
req.visited = true;
} else {
req.visited = false;
res.cookie('v', 0, {});
}
next();
}

visited 获取后传给 Pug 来设置是使用 ls('common_css') 函数还是使用 ll('common_css', 0);

1
2
3
4
5
6
7
block commonStyle
- if (!visited)
style#common_css
include ../../www/static/css/common-combo.css
script ls('common_css', 0);
- else
script ll('common_css', 0);

修改 Pug 模板中的内容,将需要缓存的公用的内容再重新分离出来,比如 headerpage-nav 等内容。然后把不同页面转换的时候要切换的文件写在一个新的 <style> 标签中。

1
2
3
4
5
6
7
8
block commonStyle
block uniqueStyle

// common-combo.css 中的样式
@import '../../layout/layout.css';
@import '../../header/header.css';
@import '../../page-nav/page-nav.css';
@import '../../footer/footer.css';

最后,我们可以比较一下使用 LocalStorage 前后传输的文件的大小差距:

我们测试的网络环境是 Good 3G (40ms, 1.5Mb/s, 750kb/s)

当没有使用 LocalStorage 缓存的时候:

我们可以看到,文档的大小是 43.4KB,其中有很大一部分是 CSS 的大小,因为我们把 header 的背景图片使用 Base64 编码添加到了样式中。当我们下次请求本页面或者其他页面的时候,添加了图片的样式已经在 LocalStorage 缓存:

我们可以看到,此时请求的文档大小只有 5.4KB,节省了网络中传输的资源。

本地存储的更新

有一些不经常修改,并且比较大的文件,我们可以使用 LocalStorage 来进行本地存储,但是不经常修改并不是代表以后都不会修改,那我们如何确定本地存储的文件是最新的版本的文件呢?我们又应该如何对本地存储的文件进行更新呢?

下面我们将针对几种本地缓存的协商策略进行对比,来选择一个合适的策略,使用在我们的博客系统上。

我的博客使用的是 Express 作为后台,Express 默认对静态的资源开启了 EtagLast-Modifiedmax-age 来支持浏览器缓存,如果我们想实现自己的缓存策略,可以选择关闭已有的缓存策略,然后自定义自己的缓存策略。

比如说,我们在开启了 Etag 之后,每一次浏览器打开该页面,还是会向服务器询问一次该文件是否更新,然后服务器返回 304 回应来声明资源没有被修改,浏览器向服务器进行请求的时候就会消耗一些交互资源。

所以,在设置更新的时候,我选择离开如下的设置方式:

1
2
3
4
Cache-Control: public, max-age=31536000
Expires: (一年后的今天)
ETag: (基于内容生成)
Last-Modified: (过去某个时间)

使用如上的方式,可以在过期时间内不发送请求而是直接使用缓存,又能保证在资源有更新的,只要通过给资源增加时间戳或者更换路径,就能让用户访问最新的资源

1
2
3
app.use(express.static(path.join(__dirname, './www/static'), {
maxAge: '365d',
}));

针对非私密性和经常性变动的资源,比如我们的 CSS 文件需要经常调整,所以我们不能设置 max-ageexpires 缓存:

1
2
3
Cache-Control: public, max-age=0
Expires: (当前时间)
ETag: (基于内容生成)

除此以外,我们还需要对存在 LocalStorage 中的数据进行更新。更新的方法是,在我们原来定义 lsll 函数的时候,需要添加一个标签定义该文件的版本号,并且将该版本号存在 cookie 中,当浏览器请求数据的时候,如果我们从 cookie 中解析得到的版本号不是最新的,则该资源还需要重新下载。所以,我们将修改 ls 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ls(name, tag) {
let target = document.getElementById(name);
if (!target) {
throw new Error('ls save fn not find ' + name);
} else {
if (window.localStorage) {
try {
window.localStorage.setItem(name, target.innerHTML);
document.cookie = 'v='+tag;
} catch(e) {
console.log(e);
}
}
}
}

也就是,我们在缓存文件的到 LocalStorage 的时候,需要赋值一个 v 参数来计算该文件的版本。在我的博客中,针对 common_css 这个文件,我们使用 MD5 算法在运行 Express 的时候自动生成一个摘要作为 v 的值,如果文件发生了变化,则该 v 的值会自动根据文件内容生成。