合理缓存SPA应用

通过合理设置缓存级别,将不同的缓存策略应用到正确的文件上提高系统性能。

Angular、React 和 Vue 的大流行导致现在越来越多的 Web 项目以单页面应用(SPA)的形式进行发布。

在前端文件的缓存方面,由于我早先是使用 PHP + Riot.js 的组合,虽然也是单页面应用,但主入口由 PHP 提供,默认就不缓存,所以只需在 nginx 中将所有静态文件强制缓存到客户端就行。

而换到 Node.js + Vue 之后,主入口换成了 index.html,并且和其他静态文件一起都是通过 express.static 提供给客户端的,所以要针对性处理才能同时满足性能和功能方面的要求。

记得之前有一次搞过头,把 index.html 也缓存了,从而影响到前端的正常更新,最终只能改回默认的缓存规则。为了尽可能加快用户访问速度,又不把事情搞砸,今天终于静下心来对其进行了合理的优化。

缓存的级别

从加载速度方面可以大致分成三种情况:

  1. 最快的是浏览器直接使用本地缓存的文件(存在客户端的内存或硬盘),通常表现为 200 OK (from memory/disk cache)

  2. 其次是与服务端核对文件是否有变化,发现没有变化再使用本地缓存的文件,通常表现为 304 Not Modified

  3. 最慢的是没有缓存可以,必须从服务器下载文件。

当用户第一次访问我们的网站时,无疑只能从服务器下载完整的文件。但是之后用户再刷新时,就要尽可能利用前两种机制,提高页面访问速度。

当前 SPA 项目的特点

以 Vue 为例,分析 Vue 项目编译后的文件,可以发现文件名是类似下面这种情况的:

app
├── css
│   ├── 10.62f583fe.css
│   ├── 3.668eb03e.css
│   ├── app.0e433876.css
│   └── vendor.301e4b97.css
├── favicon.ico
├── fonts
│   ├── KFOkCnqEu92Fr1MmgVxIIzQ.a45108d3.woff
│   ├── KFOlCnqEu92Fr1MmEU9fBBc-.cea99d3e.woff
│   └── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.2987c5cc.woff2
├── img
│   ├── logo.aea1b4b0.png
│   ├── qrcode.ee393ad7.png
│   └── top.f2e94b65.png
├── index.html
└── js
    ├── 1.b1cd2ded.js
    ├── 10.8351fd79.js
    ├── app.005e071e.js
    └── vendor.66af4e5c.js

除了 favicon.ico 和 index.html 之外,其他文件名中已经自带校验(hash),如果文件发生变化,文件名中的十六进制部分也会相应变化。

可以反推:在任意一个时间点,index.html 在文件名不变的情况下,内容可能发生变化;而其他文件只要文件名不变,内容也不变。

因此 index.html 需要和服务器比对确定内容是否变化,适用于上述第 2 种规则,其他文件可以让浏览器放心的直接使用本地缓存,适用于第 1 种规则。

💡 提示
文件名是否包含 hash 可以通过 filenameHashing 控制

💡 提示
浏览器对 favicon 的缓存策略比较特殊,且其不影响应用的实际功能,这里不予讨论。

缓存实现策略

一般情况下,配合使用 ETag 和 Cache-Control 就可以实现上面两种需求。

这里先用 Cache-Control 决定浏览器是否需要和服务进行通信来确认文件的变化情况,比如在 header 中添加 Cache-Control: max-age=86400 ,那么在 24 小时(86400秒)内,浏览器就会直接从本地缓存调用这个文件。针对目前流行的 SPA 文件自带 hash 的特点,这个 max-age 可以直接往大了设,弄个几年也无所谓。对于 index.html 之外的文件,都适用这种策略。

然后考虑 index.html 的情况,先通过 Cache-Control: max-age=0 来避免浏览器在未询问服务器的情况下直接使用本地缓存的 index.html ,也就预防了新版本上传之后,用户仍然使用浏览器缓存中的旧版本的问题。然后用 ETag 给文件一个校验码,让浏览器可以先用校验码与服务器进行比对,只在 ETag 发生变化时,才从服务器下载新版本。

💡 提示
相关理论可参考 https://web.dev/http-cache/

代码实现

下面演示如何在 Express 中实现这个缓存策略。

const express = require('express');
const app = express();

app.use(
  express.static('./public', {                                      //(1)
    etag: true,                                                     //(2)
    maxAge: '1y',                                                   //(3)
    setHeaders(res, path) {
      if (express.static.mime.lookup(path) == 'text/html') {        //(4)
        res.setHeader('Cache-Control', 'public, max-age=0');        //(5)
      }
    },
  }),
);

解释如下:

  1. 首先,我们使用 express.static 来挂载 public 目录作为静态内容目录;

  2. 为所有文件添加 ETag ,ETag 的计算方式不必深究,只要知道文件内容不变 ETag 也不变(etag 选项默认就是 true ,实际代码中可以不加,这里只是为了方便说明);

  3. maxAge: '1y' 是指将 max-age 设置成 1 年,此配置会在 header 中添加 Cache-Control: max-age=31536000

  4. 判断当前发送给客户的文件类型,是否为 html 文件

  5. 重新指定 Cache-Control 为 Cache-Control: public, max-age=0 来阻止浏览器对 index.html 无脑使用缓存。

对比

最后放上三张图来对比缓存的效果(通过 Chrome 的 Throtting 功能模拟了高速 3G 网络下的效果)

无缓存

Image

只使用ETag(express.static默认策略)

Image

优化后

Image

可以看到优化之后比默认设置获得了大约 1 秒的加载速度优势

更多

12KB的Excel导出库sheetex是怎么来的

这是一个关于前端 Excel 导出库 sheetex 的故事:我为什么要做这个库,它为什么会这么小,以及你是否值得一试。 如过你问我“为什么非要在前端导出”,那将是另一个故事。 我的数据导出史 不知道你是否还记得自己是从什么时候开始接触数据导出的? 我对自己的“数据导出史”还算有些印象:在还没有正式工作的时候,如果有人问我要数据,我会在数据库管理工具里写个查询语句,然后视对方的用途,导出成SQL 语句、CSV 文件或者Excel 等;待到工作了,需要开发面向最终用户的系统,就不能再这么手工处理,导出功能成为系统标配,用户点击一个按钮,就要下载到相应的文件。 最早是 CSV 格式,因为其生成相对容易,而且也可以通过 Excel 软件进行查看,加上主要是内部用户,偶有无法打开也只要简单培训就能解决。 但随着用户类型变得广泛起来,这种“偶尔”也逐渐变成无法忍受,那么干脆直接导出 Excel 文件吧,反正开源库也已经成熟,于是使用 SheetJS

By 熊立丁
用 Aria2 & AriaNg 搭建离线下载平台

用 Aria2 & AriaNg 搭建离线下载平台

环境 硬件:Mac Mini 2014 4C4G 操作系统:Debian 12 IP地址:192.168.2.2 流程 1. 切换到管理员权限 su root 2. 安装 aria2 apt install aria2 3. 以服务的形式运行 aria2 aria2c --enable-rpc --rpc-listen-all=true --rpc-allow-origin-all --enable-rpc 启动 rpc 服务 --rpc-listen-all=true 允许从任意 IP 访问 --rpc-allow-origin-all 允许浏览器跨域访问 4. 配置 nginx 增加站点用于放置前端界面

By 熊立丁
浙ICP备15043004号-1