浏览器缓存(强缓存与协商缓存)

浏览器缓存是前端性能优化的重要手段,主要分为强缓存协商缓存

1. 缓存整体流程

graph TD A[浏览器请求资源] --> B{检查缓存} B -->|有缓存| C[强缓存验证] B -->|无缓存| D[向服务器请求] C --> E{Cache-Control/Expires} E -->|未过期| F[直接使用缓存] E -->|已过期| G[协商缓存验证] G --> H{ETag/Last-Modified} H -->|资源未修改| I[304 Not Modified] H -->|资源已修改| J[200 OK 返回新资源] F --> K[使用缓存] I --> K J --> L[更新缓存] D --> L K --> M[渲染页面] L --> M

2. 强缓存

强缓存不会向服务器发送请求,直接从缓存中读取资源。

2.1 相关 HTTP 头部

Cache-Control (HTTP/1.1)

Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: public
Cache-Control: private

常用指令:

  • max-age=3600:资源有效期为3600秒
  • no-cache:禁用强缓存,使用协商缓存
  • no-store:完全不缓存
  • public:允许所有用户缓存
  • private:仅允许单个用户缓存

Expires (HTTP/1.0)

Expires: Wed, 21 Oct 2023 07:28:00 GMT

2.2 强缓存示例

// 服务器设置强缓存
app.get('/static/js/app.js', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年
  res.sendFile('/path/to/app.js');
});

// 或者使用 Express static 中间件
app.use('/static', express.static('public', {
  maxAge: '1y',
  etag: false // 禁用 ETag,只使用强缓存
}));

2.3 强缓存验证流程

// 浏览器检查强缓存的伪代码
function checkStrongCache(request) {
  const cache = getCache(request.url);
  
  if (!cache) return null;
  
  const cacheControl = cache.headers['cache-control'];
  const expires = cache.headers['expires'];
  
  // 优先检查 Cache-Control
  if (cacheControl) {
    if (cacheControl.includes('no-store')) return null;
    if (cacheControl.includes('no-cache')) return null;
    
    const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
    if (maxAgeMatch) {
      const maxAge = parseInt(maxAgeMatch[1]);
      const age = Date.now() - cache.timestamp;
      if (age < maxAge * 1000) {
        return cache; // 命中强缓存
      }
    }
  }
  
  // 检查 Expires
  if (expires) {
    const expiresTime = new Date(expires).getTime();
    if (Date.now() < expiresTime) {
      return cache; // 命中强缓存
    }
  }
  
  return null; // 强缓存失效
}

3. 协商缓存

当强缓存失效时,浏览器会向服务器发起验证请求,使用协商缓存。

3.1 相关 HTTP 头部

Last-Modified / If-Modified-Since

# 响应头
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT

# 请求头
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

ETag / If-None-Match

# 响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# 请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

3.2 协商缓存示例

// 服务器实现协商缓存
app.get('/api/data', (req, res) => {
  const data = getDataFromDB();
  const etag = generateETag(JSON.stringify(data));
  
  // 检查 If-None-Match
  const clientETag = req.headers['if-none-match'];
  if (clientETag === etag) {
    return res.status(304).end(); // 资源未修改
  }
  
  res.setHeader('ETag', etag);
  res.setHeader('Last-Modified', new Date().toUTCString());
  res.json(data);
});

// 生成 ETag 的函数
function generateETag(content) {
  const crypto = require('crypto');
  return crypto.createHash('md5').update(content).digest('hex');
}

3.3 协商缓存验证流程

// 协商缓存验证的伪代码
async function validateCache(request, cachedResponse) {
  const headers = {};
  
  // 添加 If-None-Match
  if (cachedResponse.etag) {
    headers['If-None-Match'] = cachedResponse.etag;
  }
  
  // 添加 If-Modified-Since
  if (cachedResponse.lastModified) {
    headers['If-Modified-Since'] = cachedResponse.lastModified;
  }
  
  try {
    const response = await fetch(request.url, {
      headers: headers,
      method: 'HEAD' // 或者 GET
    });
    
    if (response.status === 304) {
      // 资源未修改,使用缓存
      return {
        status: 'hit',
        data: cachedResponse.data,
        headers: response.headers
      };
    } else if (response.status === 200) {
      // 资源已修改,返回新数据
      const newData = await response.json();
      return {
        status: 'miss',
        data: newData,
        headers: response.headers
      };
    }
  } catch (error) {
    // 网络错误,降级使用缓存
    return {
      status: 'fallback',
      data: cachedResponse.data
    };
  }
}

4. 实际配置示例

4.1 Nginx 配置

server {
    listen 80;
    server_name example.com;
    
    # 静态资源 - 强缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Pragma "public";
        add_header Vary "Accept-Encoding";
        
        # 同时启用协商缓存
        etag on;
    }
    
    # HTML 文件 - 协商缓存
    location ~* \.(html|htm)$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
        etag on;
    }
    
    # API 接口 - 不缓存或短时间缓存
    location /api/ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
        etag on;
    }
}

4.2 Express.js 配置

const express = require('express');
const app = express();

// 静态资源 - 强缓存
app.use('/static', express.static('public', {
  maxAge: '365d',
  etag: true,
  lastModified: true,
  setHeaders: (res, path) => {
    if (path.endsWith('.html')) {
      // HTML 文件使用协商缓存
      res.setHeader('Cache-Control', 'no-cache');
    } else {
      // 其他静态资源使用强缓存
      res.setHeader('Cache-Control', 'public, max-age=31536000');
    }
  }
}));

// API 路由 - 协商缓存
app.get('/api/users', (req, res) => {
  const users = getUsersFromDB();
  const etag = generateETag(JSON.stringify(users));
  
  // 检查客户端 ETag
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('ETag', etag);
  res.json(users);
});

// 动态内容 - 不缓存
app.get('/dashboard', (req, res) => {
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
  res.render('dashboard');
});

4.3 Webpack 构建优化

// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js',
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[name].[contenthash].chunk.css',
    }),
  ],
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },
};

5. 缓存策略最佳实践

5.1 不同资源的缓存策略

资源类型 缓存策略 示例配置
HTML 文件 协商缓存 Cache-Control: no-cache
CSS/JS 文件 强缓存+版本号 Cache-Control: max-age=31536000, immutable
图片资源 强缓存 Cache-Control: max-age=2592000
API 响应 协商缓存/不缓存 Cache-Control: no-cache
用户数据 不缓存 Cache-Control: no-store

5.2 实际项目中的缓存配置

// 完整的缓存策略实现
class CacheStrategy {
  static forResourceType(filename) {
    const extension = filename.split('.').pop().toLowerCase();
    
    switch (extension) {
      case 'html':
        return {
          cacheControl: 'no-cache',
          maxAge: 0,
          etag: true
        };
        
      case 'css':
      case 'js':
        return {
          cacheControl: 'public, max-age=31536000, immutable',
          maxAge: 31536000,
          etag: false
        };
        
      case 'png':
      case 'jpg':
      case 'jpeg':
      case 'gif':
      case 'svg':
      case 'ico':
        return {
          cacheControl: 'public, max-age=2592000',
          maxAge: 2592000,
          etag: true
        };
        
      case 'woff':
      case 'woff2':
      case 'ttf':
      case 'eot':
        return {
          cacheControl: 'public, max-age=31536000',
          maxAge: 31536000,
          etag: false
        };
        
      default:
        return {
          cacheControl: 'no-cache',
          maxAge: 0,
          etag: true
        };
    }
  }
}

// 使用示例
app.use('*', (req, res, next) => {
  const strategy = CacheStrategy.forResourceType(req.path);
  res.setHeader('Cache-Control', strategy.cacheControl);
  if (strategy.etag) {
    res.setHeader('ETag', generateETag(req.path));
  }
  next();
});

6. 调试和监控

6.1 Chrome DevTools 查看缓存

// 在控制台检查资源缓存状态
function checkResourceCacheStatus(url) {
  performance.getEntriesByName(url).forEach(entry => {
    console.log('Resource:', entry.name);
    console.log('Transfer Size:', entry.transferSize);
    console.log('Encoded Body Size:', entry.encodedBodySize);
    console.log('Decoded Body Size:', entry.decodedBodySize);
    
    // 判断是否命中缓存
    if (entry.transferSize === 0 && entry.encodedBodySize > 0) {
      console.log('Status: ✅ 强缓存命中');
    } else if (entry.transferSize > 0 && entry.transferSize < entry.encodedBodySize) {
      console.log('Status: 🔄 协商缓存命中');
    } else {
      console.log('Status: ⬇️  全新下载');
    }
  });
}

6.2 缓存命中率监控

// 缓存命中率统计
class CacheMonitor {
  constructor() {
    this.stats = {
      strongCacheHits: 0,
      negotiationCacheHits: 0,
      misses: 0,
      total: 0
    };
  }
  
  recordRequest(transferSize, encodedSize) {
    this.stats.total++;
    
    if (transferSize === 0 && encodedSize > 0) {
      this.stats.strongCacheHits++;
    } else if (transferSize > 0 && transferSize < encodedSize) {
      this.stats.negotiationCacheHits++;
    } else {
      this.stats.misses++;
    }
  }
  
  getHitRate() {
    const hits = this.stats.strongCacheHits + this.stats.negotiationCacheHits;
    return (hits / this.stats.total * 100).toFixed(2);
  }
  
  getReport() {
    return {
      strongCacheHitRate: (this.stats.strongCacheHits / this.stats.total * 100).toFixed(2),
      negotiationCacheHitRate: (this.stats.negotiationCacheHits / this.stats.total * 100).toFixed(2),
      totalHitRate: this.getHitRate(),
      totalRequests: this.stats.total
    };
  }
}

通过合理配置强缓存和协商缓存,可以显著提升网站性能,减少服务器负载,改善用户体验。

posted @ 2025-10-13 21:14  阿木隆1237  阅读(31)  评论(0)    收藏  举报