缓存的最佳实践

449 阅读4分钟

对于前端缓存,文章很多。然而相当一部分文章没有什么条理,罗列了很多Cache-Control,Expires,Etag等。然而在实际使用的时候,确认很多同学看的很混乱,最后也不知道该如何制定一个实践方案。本文采取结论先行的办法,直接说明缓存的最佳实践。之后在具体说明其中内容。帮助同学先知其然,然后知其所以然。

缓存实践结论

  1. 频繁变动的资源,使用协商缓存 Cache-Control: no-cache。
HTML 文件
html页面缓存的设置主要是在< head >标签中嵌入< meta >标签,这种方式只对页面有效,对页面上的资源无效
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="Cache-Control" content="max-age=7200">

2.不常变化的资源,静态资源,使用强缓存并配合文件名添加HASH。这也是为什么前端打包的文件需要加上hash值的原因。

Cache-Control: max-age=31536000 CSS,JS,图片: 给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年) 给文件名加上hash值:

webpack给我们提供了三种哈希值计算方式:

  1. hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。(不用这个)
  2. chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
  3. contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。

那么这里就出现了两个很重要的概念。强缓存和协商缓存

强缓存

强缓存不会发送请求,直接从浏览器加载资源。 是否命中强缓存根据HTTP Response头部Expires、Cache-Control(max-age)来判断。 注意 Cache-Control(max-age)是http1.1出现的。是更合理的方案。至于Expires的不合理处如下所示。看看就行。用Cache-Control旧可以

  • Expires通过返回一个过期时间来判断是否过期,在此时间之前浏览器直接从缓存加载资源。但其缺点是返回的过期时间为服务器时间,而比较是同客户端时间比较,如果服务端和客户端存在时间误差就不准了。
  • max-age返回的时间过期时间跨度,比如max-age=3600告诉浏览器接下来的1小时内使用缓存。这样就解决了Expires时间误差导致的问题。

bvbo9td.png

服务端代码演示,koa2
app.use(async (ctx) => {
const url = ctx.request.url if (url === '/') 
    { // 访问根路径返回index.html 
        ctx.set('Content-Type', 'text/html') 
        ctx.body = await parseStatic('./index.html') } 
        else { const filePath = path.resolve(__dirname, `.${url}`) // 设置类型
        ctx.set('Content-Type', parseMime(url)) // 设置 Cache-Control 响应头 
        ctx.set('Cache-Control', 'max-age=30') // 设置传输 
        ctx.body = await parseStatic(filePath) } 
    })

**

协商缓存

请求的资源通过资源标识与服务器协商比对,协商成功,服务器返回304状态码,浏览器从本地加载。协商缓存需要用到 Etag 字段 与 if-none-matchEtag 是 HTTP 响应头中的字段,Etag 的值是根据资源内容编码生成的一段字符串(资源标识),内容不同就会生成不同的Etag。你可能在网上看到还有 Last-modified ,了解即可。

再次发起请求时,请求头会带有 if-none-match 字段,值为上一次响应的 Etag(没有则不会携带)。服务器会将请求的资源生成资源标识与发送过来的值进行比对,比如果比对成功则返回 304 状态码,浏览器从本地加载该资源。

如果协商失败服务器会返回新的资源+新的Etag(资源标识)

bvbpasd.png

app.use(async (ctx) => { 
const url = ctx.request.url 
    if (url === '/') { 
        // 访问根路径返回index.html
        ctx.set('Content-Type', 'text/html') 
        ctx.body = await parseStatic('./index.html') }
        else { const filePath = path.resolve(__dirname, `.${url}`) 
        const fileBuffer = await parseStatic(filePath)
        const ifNoneMatch = ctx.request.header['if-none-match'] 
        // 生产内容hash值 
        const hash = crypto.createHash('md5')
        hash.update(fileBuffer)
         const etag = `"${hash.digest('hex')}"`
         ctx.set('Cache-Control', 'no-cache')
         ctx.set('Content-Type', parseMime(url))
        // 对比hash值 
        if (ifNoneMatch === etag) {
            ctx.status = 304 } 
            else { 
            ctx.set('etag', etag)
            ctx.body = fileBuffer
        } 
} })

综合来说,在请求时候,先命中强缓存,失效后使用协商缓存。就概念来说cache-control,Etag,if-none-match是我们必须知道。如Expires,Last-modified为http1.0时代产物。了解就行。项目也不要使用。最后是缓存流程图。这里注意,本文只介绍前端的缓存。一般来说ng也是可以缓存。这里不做介绍

144fe8230e9.jpg

缓存分类

memory cache

Memory Cache 也就是内存中的缓存

优点:
读取速度快

缺点:
一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

disk cache

Disk Cache 也就是存储在硬盘中的缓存

优点:
缓存再硬盘中,容量大

缺点:
读取速度满

对于大文件来说,大概率是不存储在内存中的,反之优先
当前系统内存使用率高的话,文件优先存储进硬盘

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。 传输协议必须为 HTTPS Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

不常用

Push Cache

  • Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。
  • 它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂

不常用

刷新操作对缓存的影响?

  1. 正常操作(输入地址URL、跳转连接、前进后退等等):强缓存与协商缓存均有效。 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  2. 手动刷新(f5、点击菜单刷新、右键刷新):强缓存与协商缓存均有效。 因为 tab 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache
  3. 强制刷新(ctrl + f5):强缓存、协商缓存均失效