🚀 面试官:什么是强缓存与协商缓存

从浏览器网络面板的困惑到HTTP缓存的本质理解


🤔

刚开始学前端的时候遇到一个很奇怪的现象:明明修改了CSS文件,但是浏览器里看到的还是旧样式。网上搜到的信息说"清除一下缓存试试",Ctrl+F5一刷新果然就好了。但是当时很困惑:缓存到底是什么?为什么有时候会缓存,有时候不会?

后来在开发过程中发现,打开Chrome的Network面板,经常能看到一些请求显示(memory cache)(disk cache),还有些请求返回304 Not Modified。这些现象背后的原理是什么?强缓存和协商缓存又有什么区别?

再后来在很多面试中,也经常被问到这个问题。

今天就来深入了解一下HTTP缓存机制的工作原理。


📚 HTTP缓存的历史背景

🌐 Web早期的性能挑战

在互联网发展的早期,网络带宽是极其宝贵的资源。1990年代的拨号上网时代,56K的调制解调器意味着下载一张100KB的图片需要十几秒的时间。如果每次访问网页都要重新下载所有资源,用户体验将会极其糟糕。

🎯 缓存机制的诞生

HTTP缓存机制是在HTTP/1.0时代就开始设计的。最初的缓存控制主要依靠Expires头部,这是一个基于绝对时间的简单机制。但很快就发现了问题:

  • 时区差异:服务器和客户端的时间可能不同步
  • 时钟偏移:系统时间的差异导致缓存失效
  • 灵活性不足:无法精确控制缓存策略

🔄 HTTP/1.1的改进

HTTP/1.1引入了更加强大和灵活的缓存控制机制:

  • Cache-Control:基于相对时间的缓存控制
  • ETag:基于内容的缓存验证
  • Last-Modified:基于时间的缓存验证

这套机制形成了现代Web缓存的基础架构,分为强缓存协商缓存两个层次。


🎯 强缓存 vs 协商缓存:核心差异解析

💪 强缓存(Strong Cache)

强缓存是HTTP缓存的第一道防线,它的核心特点是完全不发起网络请求

🔧 工作机制:

bash 复制代码
# 浏览器请求流程
1. 浏览器检查本地缓存
2. 判断资源是否在有效期内
3. 如果有效 → 直接从缓存读取(200 from cache)
4. 如果无效 → 进入协商缓存流程

📋 关键HTTP头部:

Cache-Control(推荐):

http 复制代码
Cache-Control: max-age=3600        # 缓存1小时
Cache-Control: max-age=86400       # 缓存24小时
Cache-Control: no-cache            # 跳过强缓存,进入协商缓存
Cache-Control: no-store            # 不缓存

Expires(兼容性):

http 复制代码
Expires: Wed, 21 Oct 2024 07:28:00 GMT

🎨 在浏览器中的表现:

javascript 复制代码
// Chrome DevTools Network面板显示
Status: 200
Size: (memory cache) 或 (disk cache)
Time: 0ms

🤝 协商缓存(Negotiation Cache)

协商缓存是HTTP缓存的第二道防线,它的核心特点是需要与服务器协商,但如果资源未改变,只传输头部信息。

🔧 工作机制:

bash 复制代码
# 浏览器请求流程
1. 强缓存失效或被跳过
2. 浏览器发送条件请求到服务器
3. 服务器检查资源是否被修改
4. 如果未修改 → 返回304 Not Modified
5. 如果已修改 → 返回200和新资源

📋 两种验证方式:

ETag验证(内容哈希):

http 复制代码
# 首次请求响应
ETag: "abc123def456"

# 后续请求
If-None-Match: "abc123def456"

# 服务器响应(未修改)
Status: 304 Not Modified
ETag: "abc123def456"

Last-Modified验证(修改时间):

http 复制代码
# 首次请求响应
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# 后续请求
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

# 服务器响应(未修改)
Status: 304 Not Modified
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

📊 性能对比分析

特性 强缓存 协商缓存
网络请求 有(只传输头部)
响应时间 0-5ms 50-200ms
带宽消耗 0 几百字节
服务器压力 轻微
实时性 差(缓存期间不更新) 好(每次都检查)
适用场景 静态资源 动态内容、API

🏗️ 缓存策略的实际应用

📂 不同资源类型的缓存策略

🎨 静态资源(CSS/JS/图片)

http 复制代码
# 长期缓存策略
Cache-Control: max-age=31536000, immutable
ETag: "v1.2.3-abc123"

最佳实践:

  • 使用文件名哈希(webpack等工具自动生成)
  • 设置长期缓存(1年)
  • 内容变化时文件名自动改变

📄 HTML文件

http 复制代码
# 短期缓存或协商缓存
Cache-Control: max-age=300         # 5分钟强缓存
# 或者
Cache-Control: no-cache            # 跳过强缓存,始终协商
ETag: "page-v1.0.1"

🔄 API接口

http 复制代码
# 根据数据特性选择
Cache-Control: max-age=60          # 用户信息等相对稳定的数据
Cache-Control: no-cache            # 实时性要求高的数据
Cache-Control: no-store            # 敏感数据

🛠️ 服务器端配置示例

Nginx配置:

nginx 复制代码
# 静态资源长期缓存
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header ETag on;
}

# HTML文件协商缓存
location ~* \.html$ {
    expires 5m;
    add_header Cache-Control "public, must-revalidate";
    add_header ETag on;
}

# API接口缓存控制
location /api/ {
    add_header Cache-Control "no-cache";
    add_header ETag on;
}

Express.js配置:

javascript 复制代码
const express = require('express');
const app = express();

// 静态资源缓存
app.use('/static', express.static('public', {
  maxAge: '1y',
  immutable: true,
  etag: true
}));

// API缓存控制
app.use('/api', (req, res, next) => {
  res.set({
    'Cache-Control': 'no-cache',
    'ETag': true
  });
  next();
});

// HTML文件缓存
app.get('*.html', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=300, must-revalidate',
    'ETag': true
  });
  res.sendFile(path.join(__dirname, req.path));
});

🔍 深入探索:缓存的技术细节

🧠 浏览器缓存存储机制

Memory Cache vs Disk Cache

javascript 复制代码
// 浏览器缓存决策逻辑(简化版)
function decideCacheStorage(resource) {
  if (resource.size < 1024 * 10) {  // 小于10KB
    return 'memory';  // 内存缓存,速度最快
  }
  
  if (resource.frequency > 5) {  // 访问频繁
    return 'memory';
  }
  
  return 'disk';  // 磁盘缓存,容量大但相对较慢
}

缓存优先级

bash 复制代码
1. Memory Cache(最快,容量小)
2. Disk Cache(较快,容量大)
3. Network(最慢,实时性最好)

⚙️ ETag生成算法

服务器生成ETag的常见策略:

javascript 复制代码
// 内容哈希方式(推荐)
const crypto = require('crypto');

function generateETag(content) {
  return crypto
    .createHash('md5')
    .update(content)
    .digest('hex')
    .substring(0, 16);
}

// 文件信息方式(传统)
function generateETagFromFile(filePath, stats) {
  return `"${stats.mtime.getTime()}-${stats.size}"`;
}

// 版本号方式(语义化)
function generateSemanticETag(version, hash) {
  return `"v${version}-${hash}"`;
}

🔄 缓存更新策略

1. 版本控制策略

html 复制代码
<!-- 版本号更新 -->
<link rel="stylesheet" href="/css/style.css?v=1.2.3">
<script src="/js/app.js?v=1.2.3"></script>

<!-- 内容哈希更新(推荐) -->
<link rel="stylesheet" href="/css/style.abc123.css">
<script src="/js/app.def456.js"></script>

2. 渐进式更新策略

javascript 复制代码
// Service Worker缓存更新
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    // API请求:网络优先,缓存备用
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const responseClone = response.clone();
          caches.open('api-cache').then(cache => {
            cache.put(event.request, responseClone);
          });
          return response;
        })
        .catch(() => caches.match(event.request))
    );
  } else {
    // 静态资源:缓存优先
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetch(event.request))
    );
  }
});

🚫 常见问题与解决方案

问题1:缓存无法更新

现象:

bash 复制代码
# 修改了CSS文件但浏览器显示旧样式
style.css → 修改了颜色
浏览器 → 仍显示旧颜色(强缓存中)

解决方案:

javascript 复制代码
// 1. 开发环境禁用缓存
if (process.env.NODE_ENV === 'development') {
  app.use((req, res, next) => {
    res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
    next();
  });
}

// 2. 使用版本控制
const version = Date.now();
app.locals.version = version;
html 复制代码
<!-- 模板中使用版本号 -->
<link rel="stylesheet" href="/css/style.css?v=<%= version %>">

问题2:移动端缓存问题

现象:

bash 复制代码
# iOS Safari经常出现缓存异常
正常访问 → 显示旧页面
强制刷新 → 显示新页面

解决方案:

http 复制代码
# 添加移动端特定头部
Vary: User-Agent
Cache-Control: public, max-age=3600, stale-while-revalidate=86400

问题3:CDN缓存同步问题

现象:

bash 复制代码
# 更新了文件但CDN还是旧版本
origin服务器 → 新版本
CDN节点 → 旧版本(缓存未过期)
用户访问 → 看到旧版本

解决方案:

javascript 复制代码
// 部署时主动清除CDN缓存
const cloudflare = require('cloudflare');

async function purgeCache(files) {
  await cloudflare.zones.purgeCache(zoneId, {
    files: files
  });
}

// 或使用缓存破坏
const deployTime = Date.now();
const cdnUrl = `https://cdn.example.com/assets/app.${deployTime}.js`;

🎯 缓存策略的最佳实践

📋 缓存策略清单

DO - 推荐做法

  1. 静态资源长期缓存
http 复制代码
Cache-Control: max-age=31536000, immutable
  1. 使用内容哈希文件名
bash 复制代码
app.js → app.abc123.js
style.css → style.def456.css
  1. API响应适度缓存
http 复制代码
Cache-Control: max-age=300, stale-while-revalidate=3600
  1. HTML文件协商缓存
http 复制代码
Cache-Control: no-cache
ETag: "page-v1.0.1"

DON'T - 避免做法

  1. 不要对HTML设置长期缓存
http 复制代码
❌ Cache-Control: max-age=86400  # HTML文件
  1. 不要忽略ETag
http 复制代码
❌ 只设置 Last-Modified,不设置 ETag
  1. 不要混用绝对时间和相对时间
http 复制代码
❌ Cache-Control: max-age=3600
❌ Expires: Wed, 21 Oct 2024 07:28:00 GMT  # 同时设置

📊 现代Web开发中的缓存进化

🆕 HTTP/2和HTTP/3的影响

HTTP/2的多路复用特性减少了请求开销,但缓存仍然重要:

bash 复制代码
# HTTP/1.1时代
6个并发连接 × 100ms延迟 = 明显的性能瓶颈

# HTTP/2时代  
单连接多路复用 + 缓存 = 最佳性能

🔮 Service Worker的补充

Service Worker为缓存策略提供了更大的灵活性:

javascript 复制代码
// 分层缓存策略
const CACHE_STRATEGIES = {
  immediate: 'cache-first',      // 静态资源
  api: 'network-first',          // API数据
  pages: 'stale-while-revalidate' // 页面内容
};

self.addEventListener('fetch', event => {
  const strategy = getCacheStrategy(event.request.url);
  event.respondWith(executeStrategy(strategy, event.request));
});

📱 PWA中的离线优先

javascript 复制代码
// 离线优先的缓存策略
async function handleRequest(request) {
  try {
    // 1. 尝试从缓存获取
    const cachedResponse = await caches.match(request);
    
    // 2. 后台更新缓存
    const networkPromise = fetch(request).then(response => {
      const cache = await caches.open('v1');
      cache.put(request, response.clone());
      return response;
    });
    
    // 3. 返回缓存内容或网络内容
    return cachedResponse || networkPromise;
  } catch (error) {
    // 4. 降级到离线页面
    return caches.match('/offline.html');
  }
}

🎯 总结

🔑 核心要点

  1. 强缓存:完全不发网络请求,性能最优,适合静态资源
  2. 协商缓存:需要服务器验证,适合动态内容
  3. 缓存策略:根据资源特性选择合适的缓存政策
  4. 版本控制:使用内容哈希确保缓存的可控更新

🚀 实践建议

  • 静态资源使用长期强缓存 + 内容哈希
  • HTML文件使用协商缓存
  • API接口根据数据特性选择缓存策略
  • 开发环境禁用缓存,生产环境优化缓存

🤔 文章相关的一些文档

这是一些相关资料,如果有兴趣可以了解:

相关推荐
Pure03192 小时前
HTTP发展历程
网络·网络协议·http
你单排吧2 小时前
Uniapp之ios真机调试篇
前端·mac
布里渊区2 小时前
HTTP/2 多路复用
网络·网络协议·http
用户22152044278002 小时前
JavaScript事件循环
前端
JarvanMo2 小时前
5 个连 Remi 都不会告诉你的实用 Flutter Riverpod 技巧
前端
万少2 小时前
可可图片编辑 HarmonyOS(4)图片裁剪-canvas
前端·harmonyos
gnip2 小时前
接入高德地图
前端·javascript
WindStormrage2 小时前
崩溃埋点的实现 —— 基于 Reporting API 的前端崩溃上报
前端·性能优化
十一.3662 小时前
171-178CSS3新增
前端·javascript·css3