引言
Web 应用中,网页的加载速度是至关重要的,也是衡量Web应用的重要指标。针对此,浏览器缓存是一种常用的优化技术,可以显著提高用户的访问体验和网页的响应速度
性能影响
缓存对Web应用影响颇深:
- 减少网络请求:缓存可以存储已经下载过的资源副本,当浏览器再次请求相同的资源时,可以直接使用缓存副本,避免了网络请求的开销。这样可以减少页面加载时间和网络流量,提高网页的响应速度和性能。
- 降低服务器负载:当浏览器使用缓存副本时,减少了服务器的负载。特别是对于大型网站和高流量的应用程序,缓存可以显著减轻服务器的压力,提高系统的可扩展性和稳定性。
- 提高页面加载速度:缓存可以减少页面加载时间,因为浏览器不需要重新下载和解析已经缓存的资源。这对于移动设备和弱网环境尤为重要,可以提供更好的用户体验。
- 减少带宽消耗:缓存可以减少网络流量,可以降低用户的数据使用费用。
未使用缓存:
强缓存生效:
协商缓存生效:
强缓存
判断资源是否过期,如果没有过期则使用缓存,否则重新请求。常用于静态资源。
两种实现方式:
-
Cache-Control:通过设置
Cache-Control
响应头来控制强缓存。常见的Cache-Control
值有:public
:响应可以被任何缓存(包括浏览器缓存和 CDN)缓存。private
:响应只能被浏览器缓存缓存,不允许 CDN 缓存。max-age=<seconds>
:设置缓存的最大有效时间,单位为秒。
-
Expires:通过设置
Expires
响应头来控制强缓存。它是一个具体的过期日期时间,表示资源在该时间之前有效。但是,Expires
是一个绝对时间,它依赖于客户端和服务器时间的同步
区别:
Cache-Control
是 HTTP/1.1 中引入的缓存控制头,而Expires
是 HTTP/1.0 中引入的缓存过期时间头。Cache-Control
提供了更多的缓存控制选项和灵活性。它可以指定缓存的行为,如缓存的最大有效时间、是否允许缓存的共享等。常见的Cache-Control
值包括public
、private
、max-age
等。而Expires
只能指定一个具体的过期日期时间,它是一个绝对时间,依赖于客户端和服务器时间的同步。Cache-Control
支持更多的缓存控制指令,如no-cache
、no-store
、must-revalidate
等,可以更细粒度地控制缓存行为。而Expires
只能指定过期时间,没有其他控制选项。Cache-Control
的优先级高于Expires,应该优先考虑。
协商缓存
正式请求发起前向服务器发起一个请求,用来判断当前缓存是否有效。如果有效则使用缓存,否则重新请求。
两种实现方式:
- Last-Modified 和 If-Modified-Since:服务器在响应头中返回
Last-Modified
字段,表示资源的最后修改时间。当浏览器再次请求该资源时,会发送一个If-Modified-Since
请求头,其中包含上次获取资源时的Last-Modified
值。如果资源的最后修改时间与If-Modified-Since
的值相同,服务器会返回 304 Not Modified 响应,表示资源未发生变化,浏览器可以使用缓存副本。 - ETag 和 If-None-Match:服务器在响应头中返回
ETag
字段,表示资源的唯一标识符(通常是哈希值或版本号)。当浏览器再次请求该资源时,会发送一个If-None-Match
请求头,其中包含上次获取资源时的ETag
值。如果资源的ETag
值与If-None-Match
的值相同,服务器会返回 304 Not Modified 响应。
区别:
Last-Modified
是一个表示资源最后修改时间的响应头字段,由服务器在响应中发送。表示资源的最后修改时间。ETag
是一个表示资源唯一标识符的响应头字段,由服务器生成并在响应中发送。通常是一个哈希值或版本号,用于标识资源的特定版本。Last-Modified
是一个相对简单的机制,它只关注资源的最后修改时间。如果资源的最后修改时间发生变化,浏览器会重新请求整个资源。而ETag
提供了更精确的资源识别方式,不仅考虑资源的最后修改时间,还可以考虑其他因素,如内容的哈希值。如果ETag
值发生变化,浏览器会重新请求整个资源。ETag
的优先级高于Last-Modified
。如果服务器同时返回了ETag
和Last-Modified
,浏览器会优先使用ETag
进行缓存验证。ETag
相对于Last-Modified
具有更高的准确性和可靠性。因为有些情况下,资源的最后修改时间可能不准确(例如,文件复制或移动时可能丢失最后修改时间) ,而ETag
是由服务器生成的,不依赖于文件系统的时间戳。ETag
的计算成本相对较高,因为它需要服务器生成和发送一个唯一标识符。而Last-Modified
只需要读取文件的最后修改时间即可。
最佳实践
简单实现
js
@Controller('file')
export class FileController {
constructor(private readonly fileService: FileService) {}
sendFile(
stat: fs.Stats,
filePath: string,
res: ServerResponse,
eTag: string,
) {
res.writeHead(200, {
'Content-Type': 'image/jpeg',
'Content-Length': stat.size,
ETag: eTag,
});
fs.createReadStream(filePath).pipe(res);
}
getFileEtag(filePath: string) {
const hash = createHash('sha1');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
@Get()
getFile(@Res() res: ServerResponse, @Headers() Headers) {
const filePath = 'public/cat.jpg';
try {
const stat = fs.statSync(filePath);
if (stat.isFile()) {
const eTag = this.getFileEtag(filePath);
res.setHeader('Cache-Control', 'max-age=100');
if (Headers['if-none-match'] === eTag) {
res.statusCode = 304;
res.end();
return;
}
return this.sendFile(stat, filePath, res, eTag);
}
res.end('not found');
} catch (error: any) {
console.log(error);
res.end('error');
}
}
}