一、浏览器存储
浏览器存储是网页在用户本地保存数据的技术,主要用于实现数据持久化、减少服务器请求和离线访问能力。
1. localStorage(本地存储)
- 存储特性:提供永久性存储,容量一般在5-10MB之间,以字符串形式存储键值对。
- 生命周期:页面关闭后依然保留
2. sessionStorage(会话存储)
- 存储特性:与localStorage类似,同样存储字符串类型的键值对,容量也在5-10MB左右。
- 生命周期:其数据的有效期与页面会话相同,当页面会话结束(如浏览器关闭)时,数据将被清除。
3. cookies存储
- 存储特性:存储带过期时间的键值对。由后端通过设置Max-Age来控制有效存储时长,存储在cookies中的内容会在浏览器发送请求时自动携带在请求头中。
- 跨域能力:cookies具有跨域访问的能力,这使得它在不同域名间的数据交互中发挥作用。
4.indexedDB(客户端数据库)
- 存储特性:是一种在浏览器端的数据库,理论上存储大小无上限,能够存储表结构以及复杂的数据类型。
- 应用场景:适用于需要大量数据存储和复杂数据操作的Web应用,如离线应用、大型数据缓存等。
本地存储、会话存储、客户端数据库都不能跨域,cookies存储可以跨域。
特性 | localStorage | sessionStorage | cookies | indexedDB |
---|---|---|---|---|
数据类型 | 字符串 | 字符串 | 字符串/数值 | 复杂结构 |
存储容量 | 5-10MB | 5-10MB | ≤4KB | 理论上无限 |
生命周期 | 永久 | 会话级 | 可设置过期 | 永久 |
自动发送请求 | 否 | 否 | 是 | 否 |
同源策略 | 严格限制 | 严格限制 | 支持跨子域 | 严格限制 |
数据操作方式 | 同步API | 同步API | 同步API | 异步API |
事务支持 | 不支持 | 不支持 | 不支持 | 完整支持 |
主要应用场景 | 静态数据存储 | 临时数据暂存 | 会话管理 | 大型结构化数据 |
二、浏览器缓存(也叫HTTP缓存)
什么是浏览器缓存?
浏览器缓存是浏览器将已请求资源(如HTML、CSS、图片)暂存本地的机制。通过复用本地副本,减少重复请求,显著提升页面加载效率并降低网络消耗。
将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时,该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度。
浏览器缓存数据实际存储在本地磁盘的缓存目录中(不同浏览器路径不同)。当用户使用强制刷新时,浏览器会跳过缓存校验机制。
1. 强缓存
- 设置方式 :通过在响应头中设置
Cache-Control
字段,其值为max-age=xxx
(单位为秒),来指定缓存的有效期。 - 工作原理:当浏览器请求资源时,如果该资源仍在强缓存有效期内,则直接从浏览器的缓存中获取,无需向后端发送请求。
- 局限性 :对于通过浏览器url地址栏直接请求的资源,请求头中会自动携带
Cache-Control: max-age=0
,导致强缓存失效。
rust
const { ext } = path.parse(filePath); //获取文件后缀
res.writeHead(200, {
'content-type': mime.getType(ext), // 根据文件后缀生成对应的content-type
'cache-control': 'max-age=86400', // 强缓存 1 天
})
在强缓存有效期内,浏览器直接使用本地缓存,不会产生任何网络请求,强缓存只有在失效的那一刻才会重新发起新的请求。
只用强缓存可以把除了url地址栏访问的资源存起来,但是后端资源更新了就无法第一时间让前端获取到, 所以还需要协商缓存
2. 协商缓存
为了解决强缓存的局限性,引入了协商缓存机制。协商缓存通过在浏览器和服务器之间交换特定的头部信息,来判断资源是否需要重新获取。
(1) Last-Modified 与 If-Modified-Since
浏览器第一次访问资源时,响应头中携带last-modified
字段,该字段的值为资源的最后修改时间。当浏览器接收到响应头后,会在该资源再次被请求时,在请求头中自动携带 If-Modifed-Since
字段
- Last-Modified:服务器在响应头中携带该字段,值为资源的最后修改时间。浏览器在首次访问资源时会收到这个时间戳。
- If-Modified-Since :当浏览器再次请求同一资源时,会在请求头中自动携带该字段,其值为上次收到的
Last-Modified
值。服务器收到请求后,会比较If-Modified-Since
的值和资源当前的最后修改时间。如果两者一致,说明资源未被修改,服务器返回304
状态码,浏览器从缓存中获取资源;如果不一致,则返回200
状态码,浏览器就会重新获取最新资源。
javascript
const timeStamp = req.headers['if-modified-since'] //从请求头获取if-modified-since
let status = 200
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 文件没有发生过更改
status = 304
}
res.writeHead(status, {
'content-type': mime.getType(ext),
'cache-control': 'max-age=86400', // 强缓存 1 天
'last-modified': stats.mtimeMs, // 最后修改时间
})
if (status === 200) {
const readStream = fs.createReadStream(filePath); // 创建可读流
readStream.pipe(res); // 将可读流的数据,通过管道,输出到前端
} else {
return res.end();
}
(2)ETag 与 If-None-Match
- ETag:也叫文件指纹。服务器根据资源的内容生成一个唯一的标识(如 MD5 哈希值),并在响应头中携带该字段。浏览器首次访问时会收到这个标识。
- If-None-Match :浏览器再次请求时,在请求头中自动携带该字段,其值为上次收到的
ETag
值。服务器比较If-None-Match
的值和当前资源的ETag
值。如果一致,返回304
状态码,浏览器从缓存中获取资源;如果不一致,返回200
状态码和更新后的资源。
javascript
const ifNoneMatch = req.headers['if-none-match'] // 从请求头获取if-none-match
checksum.file(filePath, (err, sum) => {//MD5加密方式
sum = `"${sum}"`
if (ifNoneMatch === sum) { // 文件没有变化
res.writeHead(304, {
'Content-Type': mime.getType(ext),
'etag': sum,
})
res.end()
} else {
res.writeHead(200, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=1000000',
'etag': sum,
})
const resStream = fs.createReadStream(filePath)
resStream.pipe(res)
}
以上代码中,引入checksum
,计算文件的MD5值,同一份文件加密得到的MD5值是一样的,文件内容不一样,MD5值就不一样。把加密后得到的MD5值作为文件的标识Etag,并设置在响应头里面。
以上2种手段,用法基本一致,Last-Modified
是以最后一次修改时间为标识,ETag
是以文件内容作为标识。 MD5加密是要耗费性能的。大文件加密耗时比较久,所以一般采用last-modified + If-Modifed-Since
文件不是特别大且不易修改,采用文件指纹etag + If-None-Match
方案 | 优点 | 缺点 |
---|---|---|
Last-Modified | 计算成本低 | 内容不变但文件被修改也会重新发请求 |
ETag | 内容级精确验证 | 大文件计算消耗资源 |
缓存优先级
强缓存优先:浏览器始终优先检查本地强缓存,若在有效期内(Cache-Control/Expires未过期),直接使用缓存且不发送任何请求
失效触发协商:仅当强缓存过期时,才携带验证头(If-Modified-Since/If-None-Match)发起协商请求
后端资源更新了,如何让前端第一时间拿到呢?
在资源文件名后添加哈希值(如:app.3a7b6c8d.js
),当资源更新时,哈希值也会改变。这样,浏览器会将其视为不同的资源,从而重新请求和缓存。确保资源更新后URL变化,自动绕过旧缓存。
总结
HTTP 缓存机制是前端性能优化的重要组成部分。强缓存和协商缓存不是对立的,强缓存和协商缓存相辅相成,共同构成了高效的缓存策略。合理运用这些机制,可以显著提升网页的加载速度和用户体验,同时减轻服务器的负载压力。