网页为什么越刷越快?一文搞懂HTTP缓存的秘密

218 阅读10分钟

HTTP缓存机制详解:从原理到实践

你有没有发现,第一次打开一个网页时加载较慢,但刷新或再次访问时却快了很多?这背后的关键技术就是HTTP缓存。它通过减少重复的网络请求,显著提升了网页加载速度和用户体验。

本文将带你深入理解HTTP缓存的工作机制,包括强缓存与协商缓存的原理、浏览器地址栏请求的完整流程,以及它们各自的优缺点。

一、缓存的核心作用

当浏览器访问网页时,需要从服务器下载HTML、CSS、JavaScript、图片等资源。如果每次访问都重新下载,不仅浪费带宽,还会导致页面加载缓慢。

缓存的作用就是让浏览器将这些资源保存在本地(内存或硬盘),下次需要时直接使用,避免重复请求,从而:

  • 减少网络延迟
  • 降低服务器压力
  • 提升页面加载速度

二、浏览器地址栏输入URL后的完整流程

当你在浏览器地址栏输入一个网址并回车,浏览器会执行一系列步骤来加载页面。这个过程涉及缓存判断、网络请求、资源解析等多个环节。

1. 检查强缓存

浏览器首先检查本地缓存(包括内存缓存和磁盘缓存),判断请求的资源是否命中强缓存

强缓存由服务器通过响应头 Cache-ControlExpires 指令控制。例如:

Cache-Control: max-age=3600

表示该资源可缓存3600秒(1小时)。

  • 如果命中强缓存(即未过期):

    • 浏览器直接从本地缓存读取资源,不会向服务器发送请求
    • 在开发者工具的 Network 面板中,这类请求通常不会显示(除非手动刷新)。
    • 状态显示为 200 (from memory cache)200 (from disk cache)
  • 如果未命中强缓存(已过期或无缓存):

    • 浏览器进入下一步,向服务器发起请求。

注意:虽然有人认为地址栏请求不走强缓存,但实际上,只要缓存未过期,地址栏访问同样会使用强缓存。真正不使用强缓存的是“强制刷新”操作。

强缓存的实现方式与工作机制

强缓存主要通过两个HTTP响应头来实现:

  1. Cache-Control: max-age=3600
    这是现代Web开发中推荐使用的指令。max-age 指定资源在客户端缓存中的最大有效时间(单位为秒)。例如,max-age=3600 表示资源在1小时内可直接从缓存读取,无需与服务器通信。

  2. Expires: Wed, 07 Aug 2024 20:00:00 GMT
    这是一个较老的机制,指定资源的绝对过期时间。但由于它依赖客户端和服务器的时间同步,若时间不一致可能导致缓存判断错误,因此不如 Cache-Control 灵活可靠。

工作机制
浏览器在收到带有强缓存头的响应后,会将资源及其过期时间保存在本地。当下次请求同一资源时,浏览器首先检查当前时间是否在有效期内。如果仍在有效期内,就直接使用缓存,完全跳过网络请求,实现“零延迟”加载。

首次加载,我们观察发现,image.png图片的加载用时为15ms。

image.png

第二次加载,图片的加载变为了0ms。

image.png

优点

  • 性能最优:无需任何网络请求,加载速度最快,用户体验极佳。
  • 节省带宽:大幅减少服务器和网络的负载,尤其对静态资源密集的网站意义重大。

缺点

  • 更新不及时:一旦设置了较长的缓存时间(如1年),即使服务器上的资源已经更新,客户端仍可能长时间使用旧版本。
  • 需要配合版本策略:为解决更新问题,通常采用“文件名加哈希”的方式,例如将 app.js 构建为 app.a1b2c3.js。当内容变化时,哈希值改变,文件名也随之变化,浏览器会认为这是一个新资源,从而触发重新下载。

2. 发起HTTP请求(强缓存未命中时)

如果强缓存未命中,浏览器将向服务器发起完整的HTTP请求,流程如下:

(1)DNS解析

浏览器先查找域名对应的IP地址。它会依次检查:

  • 浏览器自身缓存
  • 操作系统缓存
  • 路由器缓存
  • ISP的DNS服务器

如果都未命中,则向根DNS服务器发起查询。

(2)建立TCP连接

浏览器与服务器通过“三次握手”建立TCP连接。如果是HTTPS请求,还需进行TLS握手,协商加密参数。

(3)发送HTTP请求

浏览器发送GET请求,例如:

GET /index.html HTTP/1.1
Host: example.com
(4)服务器处理并返回响应

服务器处理请求后返回资源和响应头,可能包含:

HTTP/1.1 200 OK
Cache-Control: max-age=3600
Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
ETag: "abc123"

3. 协商缓存(服务器验证资源是否更新)

如果强缓存未命中,但浏览器本地有旧资源,它会尝试使用协商缓存机制,询问服务器资源是否已更新。

协商缓存的实现方式

协商缓存有两种主要实现方式:

  1. Last-Modified / If-Modified-Since

    • 服务器在首次返回资源时,通过 Last-Modified 头告知资源的最后修改时间,例如:
      Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
      
    • 当缓存过期后,浏览器再次请求该资源时,会在请求头中携带:
      If-Modified-Since: Wed, 21 Oct 2020 07:28:00 GMT
      
      表示“我本地的版本是这个时间修改的,你们服务器上有没有比这更新的?”
    • 服务器收到后,比较当前资源的修改时间:
      • 如果没有更新,返回 304 Not Modified,浏览器继续使用本地缓存。
      • 如果有更新,返回 200 OK 和新资源。

    在 Node.js 等后端环境中,可以通过 fs.stat('file.txt').mtimeMs 获取文件的最后修改时间(毫秒时间戳),并将其作为 Last-Modified 的值。

   // 假设 stat 是 fs.stat 获取的文件信息
const ifModifiedSince = req.headers['if-modified-since'];
const fileLastModified = stat.mtime; // Date 对象

// 如果客户端发送了 If-Modified-Since 头
if (ifModifiedSince) {
  const clientTime = new Date(ifModifiedSince); // 转换为 Date 对象
  // 比较到秒级 (除以1000取整)
  if (Math.floor(clientTime.getTime() / 1000) === Math.floor(fileLastModified.getTime() / 1000)) {
    // 文件未修改,返回 304
    res.writeHead(304, {
      'Cache-Control': 'max-age=86400',
      'Last-Modified': fileLastModified.toUTCString(), // 必须是字符串
    });
    res.end(); // 304 响应不能有响应体
    return; // 重要:返回,避免执行后续发送文件的代码
  }
}

// 文件有修改或首次请求,返回 200 和文件
res.writeHead(200, {
  'Content-Type': mime.getType(ext),
  'Cache-Control': 'max-age=86400',
  'Last-Modified': fileLastModified.toUTCString(), // 必须是字符串
});

// 创建并发送文件流
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// 注意处理流错误
fileStream.on('error', (err) => {
  res.statusCode = 500;
  res.end('Internal Server Error');
});
  1. ETag / If-None-Match
    • ETag 是一个更精确的验证机制。服务器根据资源内容生成一个唯一标识(通常是哈希值),例如:
      ETag: "abc123"
      
    • 浏览器下次请求时带上:
      If-None-Match: "abc123"
      
    • 服务器对比当前资源的 ETag
      • 如果一致,返回 304,浏览器使用缓存。
      • 如果不一致,返回 200 和新资源。
  checksum.file(filePath, (err, hash) => {
  if (err) {
    // 处理计算哈希出错的情况
    res.statusCode = 500;
    res.end('Internal Server Error');
    return;
  }

  const etag = `"${hash}"`; // 生成 ETag
  const ifNoneMatch = req.headers['if-none-match'];

  // 如果客户端发送了 If-None-Match 头且与当前 ETag 匹配
  if (ifNoneMatch === etag) {
    // 文件未修改,返回 304
    // 注意:304 响应通常不包含 Content-Type
    res.writeHead(304, {
      'Cache-Control': 'max-age=86400',
      'ETag': etag,
    });
    res.end(); // 304 响应不能有响应体
    return; // 重要:返回,避免执行后续代码
  }

  // ETag 不匹配或首次请求,返回 200 和文件
  res.writeHead(200, {
    'Content-Type': mime.getType(ext),
    'Cache-Control': 'max-age=86400',
    'ETag': etag,
  });

  // 创建并发送文件流
  const fileStream = fs.createReadStream(filePath);
  fileStream.pipe(res);
  fileStream.on('error', (err) => {
    res.statusCode = 500;
    res.end('Internal Server Error');
  });
});

ETag 的优势在于它基于内容而非时间,即使文件修改时间变了但内容未变(比如服务器时间不准),也不会误判。

工作机制
协商缓存的核心是“先问后用”。浏览器每次请求都与服务器“协商”,确认资源是否变化。如果未变,服务器返回304状态码,不带响应体,浏览器继续使用本地缓存。这种方式在保证内容一致性的同时,尽可能减少了数据传输。

优点

  • 内容一致性高:能准确判断资源是否更新,避免用户看到过期内容,特别适合内容可能频繁变动的资源。
  • 灵活性强:适用于那些不能长期缓存但又希望减少流量的场景。

缺点

  • 仍有网络开销:虽然304响应体很小,但每次请求仍需与服务器通信,存在一定的延迟,尤其在网络较差时感知明显。
  • 服务器压力较大:相比强缓存,服务器需要参与每次验证,增加了计算和I/O负担。

4. 加载与渲染资源

  • HTML文档:浏览器解析HTML,构建DOM树,开始页面渲染。
  • 子资源(CSS、JS、图片等)
    • 如果命中强缓存,直接从缓存加载。
    • 否则,重新发起请求,可能触发协商缓存。

5. 更新本地缓存

如果服务器返回了新的资源(200状态码),浏览器会根据响应头中的缓存策略(如 Cache-Control)更新本地缓存,供下次使用。


三、不同用户操作的缓存行为

用户操作缓存检查方式是否发送请求
地址栏输入URL回车先检查强缓存,未命中再协商缓存视缓存情况而定
普通刷新(F5 / 刷新按钮)忽略强缓存,直接发送请求,触发协商缓存
强制刷新(Ctrl+F5)忽略所有缓存,强制从服务器重新下载

四、实际开发中的缓存策略建议

在真实项目中,我们需要根据不同资源的特点制定合理的缓存策略:

资源类型推荐策略原因
HTMLno-cache 或短 max-age(如60秒)HTML是页面的骨架,通常包含动态内容或链接,需要确保用户获取最新结构。
CSS/JS长时间强缓存(如 max-age=31536000) + 文件名加哈希静态资源稳定,通过构建工具生成带哈希的文件名(如 main.abcd1234.js),内容变化时文件名改变,浏览器会重新下载。
图片/字体长时间强缓存(如 max-age=2592000这些资源通常不会频繁修改,适合长期缓存以提升性能。

小技巧:现代前端构建工具(如Webpack、Vite)都支持自动为静态资源生成内容哈希,开发者只需配置即可。这种“内容哈希”策略完美解决了强缓存带来的更新延迟问题。