利用HTTP缓存加速Remix应用

1,485 阅读5分钟

HTTP缓存相信大家都不陌生。ETag, Cache-Control等关键词也在提到HTTP缓存时立马出现在脑海中。但很多人可能并没有真正的配置过,至少我在做SSR项目前,几乎没有在真正的项目里配置过。

一是因为CSR项目需要配缓存的只有静态资源 - JS,CSS,图片等。而这些都是直接托管在CDN上,CDN的缓存配置基本已经配好了。

二是因为这些缓存配置一般都是由专门的运维同学维护的,我们前端开发并不会直接去更改。

SSR改变了这两点:

一是我们可以给HTML加上缓存

二是我们可以动态的,根据每个项目,甚至同一个项目里不同的页面,加上不同的缓存策略

接下来我们通过一个非常简单的nodejs服务来看下怎么给HTML加上缓存

const { createServer } = require("http");
const server = createServer((req, res) => {
  const html = createPage("home");
  res.end(html);
});

server.listen(3000);

function createPage(title) {
  return `
    <!doctype html>
    <html>
      <head>
        <meta charset=utf-8 />
        <title>${title}</title>
      </head>
      <body>
        <h1>${title}</h1>
        ${Array.from({ length: 1000 })
          .map(() => "<div>stupic junk</div>")
          .join("\n")}
      </body>
    </html>
  `;
}

Array.from部分是只是为了增加一下HTML的体积,模拟一下真实的场景

用浏览器访问一下我们的应用,看看网络请求

Screen Shot 2022-10-30 at 00 28 57

访问一次我们的页面需要下载23.3KB的资源

我们没有加任何的缓存相关的响应头,如果再次访问我们的页面,此时浏览器会怎么处理呢?没有缓存吗?是也不是。

如果我们刷新页面,从网络面板可以看出每一次请求都直接到了服务器,没有缓存 - 这是是 但如果我们点击浏览器的后退/前进按钮来访问页面呢?此时浏览器是有缓存的 - 这是不是。想了解这个缓存的同学可以参考bfcache,本文不过多探讨。

我们可以直接加上Cache-Control: no-store响应头,告诉浏览器不要缓存。此时bfcache会失效,每一次访问都不会走缓存

const { createServer } = require("http");
const server = createServer((req, res) => {
  const html = createPage("home");
  res.writeHead(200, {
    "Cache-Control": "no-store",
  });
  res.end(html);
});

etag

不过每次访问都要下载23.3KB也是挺浪费的,来加一个协商缓存吧。我们加一个etag来看下协商缓存是怎么一回事

const { createServer } = require("http");
const server = createServer((req, res) => {
  const html = createPage("home");
  res.writeHead(200, {
    "Cache-Control": "no-cache",
    "etag": "12345678"
  });
  res.end(html);
});

image

当我们再一次访问这个链接时,浏览器在请求头里就会带上这个etag

image

所以我们可以在服务端做一个判断,如果请求头里的If-None-Matchetag相等,我们就可以直接返回304状态码。

const { createServer } = require("http");
const server = createServer((req, res) => {
  const html = createPage("home");
  if (req.headers["if-none-match"] === "12345678") {
    res.writeHead(304);
    res.end();
  }
  res.writeHead(200, {
    "Cache-Control": "no-cache",
    etag: "12345678",
  });
  res.end(html);
});

image

可以看到,此时再访问时网络传输大小只有113B

从上面的例子可以看到,etag是一个资源的标识,可以用来判断资源内容是否有变化。所以在实际应用中我们需要根据资源内容来生成

const { createServer } = require("http");
const { createHash } = require("crypto");

function md5(str) {
  return createHash("md5").update(str).digest("hex");
}

const server = createServer((req, res) => {
  const html = createPage("home");
  const etag = md5(html);
  if (req.headers["if-none-match"] === etag) {
    res.writeHead(304);
    res.end();
  }
  res.writeHead(200, {
    "Cache-Control": "no-cache",
    etag,
  });
  res.end(html);
});

max-age

我们也可以给HTML加上强缓存

const { createServer } = require("http");

const server = createServer((req, res) => {
  const html = createPage("home");
  res.writeHead(200, {
    "Cache-Control": "max-age=60",
  });
  res.end(html);
});

server.listen(3000);

max-age=60就是缓存60s。在60s内再次请求会直接走本地缓存,不会请求到服务器 image

不过给HTML加上缓存是需要万分注意的。HTML不像其他静态资源,可以通过改文件名的形式使缓存失效。一旦在浏览器中缓存,只能等缓存过期,或者用户主动删除缓存。所以HTML的缓存时间一般会设置的比较短,主要原因是max-age缓存是直接存在用户浏览器里的,一旦缓存,我们就没法主动删除了。

s-maxage

有没有一种缓存既可以用来缓存HTML,又受我们控制,可以主动删除的呢?CDN缓存。

一般来讲,用户通过域名访问我们的服务,是会经过一层CDN的。

image

我们可以利用Cache-Control: s-maxage=3600响应头,让CDN缓存我们的HTML,而浏览器不缓存。这样有两个好处

  1. 其他用户访问到这个CDN节点时,CDN会直接利用缓存。我们的服务器压力瞬间减小
  2. CDN缓存我们是可以主动更新和失效的,是受我们控制的

不过每个公司的基建都不一样,是否支持s-maxage可能需要咨询一下你们的运维了。另外max-ages-maxage是可以公用的,这一点就交给读者去探索了

另一个有意思的缓存指令是stale-while-revalidate,即在缓存失效时先返回缓存给用户,同时在后台去请求服务端更新本地缓存。这也是swr请求库的理念来源。不过这个缓存指令比较新,浏览器兼容性是个问题,CDN也不一定支持

Remix

现在我们可以给Remix应用加上HTTP缓存了。非常简单,给路由加上一个headers的方法就可以了


export function headers() {
  return {
    "Cache-Control": "max-age: 60"
  }
}

export default function SomeRoute() {
  return <div>some route page</div>
}

我们可以根据实际情况,给不同的路由加上不同的缓存时间。以我的博客为例,首页可以缓存1天,详情页可以缓存7天,而关于页可以缓存1个月。在Remix中做到这一点非常简单,只需要在各自路由的headers里配置就可以了