浏览器 HTTP 缓存是一种常见的 web 性能优化的手段,也是在前端面试中经常被考察的一个知识点。本文通过配置 koa2 服务器,在实践中带你探究浏览器的 HTTP 缓存机制。
先来直观认识一下浏览器HTTP缓存:
上面是打开浏览器后直接访问 V2EX 首页后的截图,我矩形圈起来的那块也就是 size 部分显示的都是 from disk cached ,说明这些资源命中了强缓存,强缓存的状态码都是 200。
再来看看我直接访问上面箭头指向的那张图片是什么情况:
可以看到返回码是 304,并且请求的时候带上了协商缓存用于协商的两个请求头 if-modified-since 和 if-none-match,命中了协商缓存。可能有部份读者看到这里不太理解我前面提到的强缓存和协商缓存是什么鬼,没关系,看到最后再回过头来看,你就自然能清晰的看懂我上面圈起来的东西和提到的一些不懂术语。
缓存判断规则是怎么实现的
我们知道浏览器和服务器进行交互的时候会发送一些请求数据和响应数据,我们称之为HTTP报文。
与缓存相关的规则信息就包含在报文首部 header 中。下面是 chrome network 面板中的信息:
浏览器的 HTTP 缓存协议本质上就通过请求响应过程中在 header 中携带那些和缓存相关的字段来实现的。
浏览器 HTTP 缓存的分类
浏览器HTTP缓存分两钟:
强缓存
协商缓存
强缓存指的是浏览器在本地判定缓存有无过期,未过期直接从内存和磁盘读取缓存,整个过程不需要和服务器通信。
协商缓存需要向服务器发送一次协商请求,请求时带上和协商缓存相关的请求头,由服务器判断缓存是否过期,未过期就返回状态码 304,浏览器当发现响应的返回码是 304,也直接是读取本地缓存,如果服务器判定过期就直接返回请求资源,状态码为 200。
浏览器请求资源是缓存的判定的简略流程如下图:
文字解释一下:当浏览器请求一个资源时,浏览器会先从内存中或者磁盘中查看是否有该资源的缓存。如果没有缓存,可能浏览器之前没访问过这个资源或者缓存被清除了那只能向服务器请求该资源。
如果有缓存,那么就先判断有没有命中强缓存。如果命中了强缓存则直接使用本地缓存。如果没有命中强缓存但是上次请求该资源时返回了和协商缓存相关的响应头如 last-modified 那么就带上和协商缓存相关的请求头发送请求给服务器,根据服务器返回的状态码来判定是否命中了协商缓存,命中了的话是用本地缓存,没有命中则使用请求返回的内容。
强缓存和协商缓存的区别
命中时状态码不同。强缓存返回 200,协商缓存返回 304。
优先级不同。先判定强缓存,强缓存判断失败再判定协商缓存。
强缓存的收益高于协商缓存,因为协商缓存相对于强缓存多了一次协商请求。
演示服务器说明
整个 koa2 演示服务器在这: koa2-browser-HTTP-cache 。总共就几个文件,index.js 入口文件,index.html 首页源代码,sunset.jpg 和 style.css 是 index.html 用到的图片和样式。
服务器代码代码很简单,使用 koa-router 配了三个路由,目前还没有写缓存相关的代码。
// src/index.js
const Koa = require('koa');
const Router = require('koa-router');
const mime = require('mime');
const fs = require('fs-extra');
const Path = require('path');
const app = new Koa();
const router = new Router();
// 处理首页
router.get(/(^\/index(.html)?$)|(^\/$)/, async (ctx, next) => {
ctx.type = mime.getType('.html');
const content = await fs.readFile(Path.resolve(__dirname, './index.html'), 'UTF-8');
ctx.body = content;
await next();
});
// 处理图片
router.get(/\S*\.(jpe?g|png)$/, async (ctx, next) => {
const { path } = ctx;
ctx.type = mime.getType(path);
const imageBuffer = await fs.readFile(Path.resolve(__dirname, `.${path}`));
ctx.body = imageBuffer;
await next();
});
// 处理 css 文件
router.get(/\S*\.css$/, async (ctx, next) => {
const { path } = ctx;
ctx.type = mime.getType(path);
const content = await fs.readFile(Path.resolve(__dirname, `.${path}`), 'UTF-8');
ctx.body = content;