鲁斯前端布鲁斯前端

文章中英模式

常见的前端面试题目 - 用户交互优化 缓存策略

深入解析前端 runtime 时期的缓存机制,包含 HTTP 缓存、Web Storage 及离线策略,为面试提供完整的缓存知识与实战应用。

影片縮圖

懒得看文章?那就来看视频吧

基本概念

缓存(Cache)是临时存储数据的技术,可以加速资源的获取、减少不必要的网络请求,并提升用户体验。在前端开发中,runtime 时期的缓存策略是指在浏览器运行期间如何有效管理和使用临时存储的资源和数据。

用户请求资源
检查缓存是否有所需资源
     ├─→ 有:直接从缓存读取 (Cache Hit)
     │    └─→ 返回资源,节省网络请求
     └─→ 没有:向远程服务器请求 (Cache Miss)
          └─→ 获取资源 → 存入缓存 → 返回资源

浏览器内建缓存机制

HTTP 缓存

浏览器的 HTTP 缓存是最基本且强大的缓存机制,主要通过 HTTP 标头来控制。

常见的缓存控制标头:

Cache-Control: 设置缓存行为
Expires: 设置资源过期时间
ETag: 资源版本标识符
Last-Modified: 资源最后修改时间

Cache-Control 详解

Cache-Control 是 HTTP/1.1 中最重要的缓存控制标头,提供了多种指令:

指令说明
max-age资源在指定秒数内视为新鲜,可直接使用缓存
no-cache必须先与服务器确认资源是否有更新,再使用缓存
no-store完全不使用缓存,每次都重新请求
public表示响应可被任何缓存区缓存,包括代理服务器
private表示响应只可被浏览器缓存,不可被中间代理缓存
must-revalidate过期后必须向源服务器验证

no-cache 与 no-store 的区别

no-cache
  • 1. 不是「不缓存」,而是「不直接使用缓存」
  • 2. 资源会被存储在缓存中
  • 3. 使用前会发送条件请求(使用 ETag 或 Last-Modified)向服务器确认资源是否有更新
  • 4. 如未更新,服务器返回 304 状态码,浏览器使用缓存
  • 5. 适用于:经常变化但大部分时间保持不变的内容(如新闻页面)
no-store
  • 1. 完全不使用缓存
  • 2. 不存储任何响应
  • 3. 每次请求都从服务器获取完整资源
  • 4. 适用于:敏感信息、持续变化的数据(如即时股票价格)
┌─────────────────────────────────────────────────────────────┐
│ no-cache vs no-store 工作流程                                │
└─────────────────────────────────────────────────────────────┘

【首次请求】
  Browser                  Server
     │                       │
     │─────── 请求资源 ─────>│
     │                       │
     │<── 返回资源+标头 ────│
     │                       │

【再次请求 (no-cache)】
  Browser                  Server
     │                       │
     │── 条件请求(带ETag) ─>│
     │                       │
     │<── 304 Not Modified ─│ (资源未变)
     │     或 200 (已变更)    │
     │                       │

【再次请求 (no-store)】
  Browser                  Server
     │                       │
     │─────── 请求资源 ─────>│
     │                       │
     │<── 返回完整资源 ────│ (总是返回完整资源)
     │                       │

ETag 工作机制

ETag 是服务器生成的资源版本标识符,用于检测资源是否有变化:

  1. 1. 服务器返回资源时附带 ETag 值(通常是内容的哈希值)
  2. 2. 浏览器下次请求相同资源时,发送 If-None-Match 标头,值为之前收到的 ETag
  3. 3. 服务器比对 ETag:
    • 1. 若匹配(资源未变化):返回 304 状态码,不返回实体内容
    • 2. 若不匹配(资源已变化):返回 200 状态码和完整资源,以及新的 ETag
┌─────────────────────────────────────────────────────────────┐
│ ETag 工作流程                                                │
└─────────────────────────────────────────────────────────────┘

【首次请求】
Browser                                    Server
   │                                         │
   │────────── GET /image.jpg ───────────>  │
   │                                         │
   │<── HTTP/1.1 200 OK                      │
   │    ETag: "33a64df551425fcc55e4d42a"    │
   │    (图片数据...)                         │
   │                                         │

【再次请求】
Browser                                     Server
   │                                          │
   │── GET /image.jpg                         │
   │   If-None-Match: "33a64df551425fcc55e4d42a" ─>│
   │                                          │ ┌─ 检查ETag
   │                                          │ │  是否匹配
   │                                          │ ↓
   │<── HTTP/1.1 304 Not Modified            │ 匹配!
   │    ETag: "33a64df551425fcc55e4d42a"     │
   │    (无实体内容)                          │
   │                                          │
缓存控制示例:
// 服务器端设定缓存标头 (Node.js Express 示例)
app.get('/api/data', (req, res) => {
  // 设定缓存一小时 (3600秒)
  res.setHeader('Cache-Control', 'max-age=3600');
  res.json({ data: 'cached content' });
});

// 前端请求时加入缓存控制
fetch('/api/user', {
  cache: 'force-cache', // 强制使用缓存
  // 或使用 'no-cache' 强制验证,'no-store' 不使用缓存
})

缓存控制策略图:

┌─────────────────────────────────────────────────┐
│ 缓存策略判断流程                                  │
└─────────────────────────────────────────────────┘
                  请求资源
              检查 Cache-Control
         ┌───────────┴───────────┐
         ↓                       ↓
   no-store存在                 检查其他指令
  (完全不缓存)                     │
         │             ┌─────────┴─────────┐
         │             ↓                   ↓
         │         max-age未过期          max-age已过期
         │        (直接使用缓存)               │
         │             │                     │
         │             │                 有ETag/Last-Modified
         │             │                     │
         │             │                     ↓
         │             │               向服务器发送条件请求
         │             │                     │
         │             │            ┌────────┴────────┐
         │             │            ↓                 ↓
         │             │      304 Not Modified   200 更新内容
         │             │      (继续使用缓存)     (更新缓存并显示)
         ↓             ↓            ↓                 ↓
     发起网络请求  显示缓存内容    显示缓存内容       显示新内容

本地存储 (Web Storage)

除了 HTTP 缓存,浏览器也提供了多种本地存储机制,用于存储和缓存数据。

Web Storage 比较表

特性localStoragesessionStorageIndexedDBCookies
存储容量~5MB~5MB无限制 (实际取决于硬盘空间)~4KB
生命周期永久存储,除非手动删除页面会话结束后删除永久存储,除非手动删除可设定过期时间
数据类型字符串字符串几乎所有 JS 数据类型,包括二进制字符串
适用场景持久化配置、主题偏好表单数据暂存、页面间传值大量结构化数据、离线应用身份验证、跟踪

Web Storage 使用示例

// localStorage 基本操作
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');

// sessionStorage 基本操作
sessionStorage.setItem('formData', JSON.stringify({ name: '張三' }));
const formData = JSON.parse(sessionStorage.getItem('formData'));

// 封裝 Storage 操作
class StorageUtil {
  static set(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  static get(key) {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  }
  
  static remove(key) {
    localStorage.removeItem(key);
  }
  
  static clear() {
localStorage.clear();
  }
}

注意事项:

  • 1. Web Storage 只能存储字符串,需要使用 JSON.stringify() 和 JSON.parse() 处理复杂数据
  • 2. 注意存储容量限制,大型数据建议使用 IndexedDB
  • 3. 敏感数据不要存储在本地,容易被攻击者获取

IndexedDB

IndexedDB 是一个低级 API,用于客户端存储大量结构化数据。它是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。

IndexedDB 基本用法

// 打开数据库
const request = indexedDB.open('MyDatabase', 1);

// 处理数据库升级
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建对象仓库(类似于表)
  const store = db.createObjectStore('users', { keyPath: 'id' });
  // 创建索引
  store.createIndex('name', 'name', { unique: false });
};

// 数据库操作
request.onsuccess = (event) => {
  const db = event.target.result;
  
  // 写入数据
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  store.add({
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com'
  });
  
  // 读取数据
  const getRequest = store.get(1);
  getRequest.onsuccess = () => {
    console.log(getRequest.result);
  };
  
  // 使用索引查询
  const nameIndex = store.index('name');
  const nameQuery = nameIndex.get('张三');
  nameQuery.onsuccess = () => {
    console.log(nameQuery.result);
  };
};

// 错误处理
request.onerror = (event) => {
  console.error('数据库错误:', event.target.error);
};

IndexedDB 应用场景

  • 1. 离线应用数据存储:保存应用数据,实现离线功能
  • 2. 大文件/二进制数据:存储图片、音频等大型文件
  • 3. 结构化数据查询:需要索引和复杂查询的数据管理
  • 4. 客户端缓存:缓存API响应,提升应用性能
注意事项
  • 1. IndexedDB 操作是异步的,需要正确处理回调或使用 Promise 封装
  • 2. 注意浏览器的存储限制,通常是可用磁盘空间的一定比例
  • 3. 实现数据同步机制,确保离线数据能够与服务器同步

数据请求缓存策略 (SWR)

SWR 是一种现代的数据获取策略,先返回缓存数据(stale),然后发送请求获取最新数据(revalidate):

┌─────────────────────────────────────────────────┐
│ SWR (Stale-While-Revalidate) 请求流程            │
└─────────────────────────────────────────────────┘
                  组件渲染
              检查缓存是否有数据
         ┌───────────┴───────────┐
         ↓                       ↓
    有缓存数据                 无缓存数据
         │                       │
         ↓                       ↓
    立即返回缓存数据          显示加载状态
         │                       │
         │                       │
         │                       │
         └───────────┬───────────┘
              发起网络请求获取新数据
         ┌───────────┴───────────┐
         ↓                       ↓
    请求成功                   请求失败
         │                       │
         ↓                       ↓
    更新缓存数据              显示错误状态
    使用新数据更新UI              
                     
              自动重新验证触发条件:
              - 窗口聚焦时
              - 网络重连时
              - 定时刷新
如何解决前端缓存更新的问题?

前端缓存更新是一个常见的挑战,有以下几种解决方案:

  1. 1. 使用版本号或哈希值
    <script src="app.3f8a7b9c.js"></script>
    
    <script src="app.js?v=1.2.3"></script>
  2. 2. 设置合适的缓存控制标头
    // HTML - 不快取或快取時間短
    Cache-Control: no-cache, must-revalidate
    
    // CSS/JS - 長時間快取,但使用版本化URL
    Cache-Control: max-age=31536000, immutable
  3. 3. Service Worker 版本管理
    // sw.js
    const CACHE_VERSION = 'v1.2.3';
    const CACHE_NAME = `app-shell-${CACHE_VERSION}`;
    
    self.addEventListener('activate', event => {
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames
              .filter(name => name.startsWith('app-shell-'))
              .filter(name => name !== CACHE_NAME)
              .map(name => caches.delete(name))
          );
        })
      );
    });
  4. 4. API 数据的刷新策略
    // SWR 策略:先显示缓存数据,然后在后台刷新
    function fetchData(url) {
      // 从缓存获取数据并立即显示
      const cachedData = localStorage.getItem(url);
      if (cachedData) {
        renderData(JSON.parse(cachedData));
      }
      
      // 同时发起新请求获取最新数据
      fetch(url)
        .then(res => res.json())
        .then(newData => {
          // 更新缓存
          localStorage.setItem(url, JSON.stringify(newData));
          // 更新显示
          renderData(newData);
        });
    }
  5. 5. 手动强制更新
    // 提供用户手动刷新功能
    function forceRefresh() {
      // 清除特定数据缓存
      localStorage.removeItem('user-data');
      
      // 或者重新加载页面,绕过缓存
      location.reload(true);
    }

实际应用场景

┌─────────────────────────────────────────────────┐
│ 电商网站商品页面的缓存策略                        │
└─────────────────────────────────────────────────┘
  
  1. 静态资源 (CSS/JS/图片)
     ├─→ HTTP 缓存: Cache-Control: max-age=86400 (一天)
     └─→ Service Worker: 预缓存核心资源,支持离线访问
  
  2. 商品基本信息
     ├─→ localStorage: 最近浏览的20件商品
     └─→ SWR: 显示缓存资料同时背景重新验证
     
  3. 用户个性化内容 (购物车/收藏)
     ├─→ 短期缓存: Cache-Control: max-age=60 (一分钟)
     
  4. 搜索结果/推荐
     └─→ sessionStorage: 存储本次会话内的搜索结果

🔥 常见面试题目

(一)有哪些HTTP 缓存策略?「强缓存」和「协商缓存」是什么?

解答:

强缓存(Strong Cache)和协商缓存(Negotiation Cache)是 HTTP 缓存的两种主要机制:

强缓存
  • 1. 直接使用本地缓存,不与服务器通讯
  • 2. 主要通过 Cache-Control Expires 标头控制
  • 3. 响应速度最快,减轻服务器负担
  • 4. 例如:Cache-Control: max-age=3600(缓存1小时内有效)
协商缓存
  • 1. 浏览器先询问服务器资源是否变更
  • 2. 使用 ETag Last-Modified 标头与服务器协商
  • 3. 如未变更,服务器返回 304 Not Modified,浏览器使用本地缓存
  • 4. 如已变更,服务器返回 200 OK 和完整资源
  • 5. 例如:浏览器发送 If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

(二)请解释 Cache-Control 中 no-cache 和 no-store 的区别

解答:

实际应用决策:

  • 1. 若数据经常变化,但仍希望利用缓存减轻服务器负载:使用 no-cache
  • 2. 若数据非常敏感或每次必须显示最新数据:使用 no-store

(三)如何实现前端应用的离线功能?

解答:

实现前端应用离线功能主要依靠 Service Worker 和各种存储 API:

  1. 1. Service Worker 注册与安装
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js')
        .then(reg => console.log('SW registered'))
        .catch(err => console.error('SW registration failed', err));
    }
  2. 2. 预缓存核心资源
    // sw.js
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open('app-shell').then(cache => {
          return cache.addAll([
            '/',
            '/index.html',
            '/styles.css',
            '/app.js',
            '/offline.html'
          ]);
        })
      );
    });
  3. 3. 离线响应策略
    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            // 缓存优先,网络作为后备方案
            return response || fetch(event.request)
              .then(response => {
                return caches.open('dynamic-content')
                  .then(cache => {
                    cache.put(event.request.url, response.clone());
                    return response;
                  });
              })
              .catch(() => {
                // 如果是页面请求,返回离线页面
                if (event.request.mode === 'navigate') {
                  return caches.match('/offline.html');
                }
              });
          })
      );
    });
  4. 4. 数据同步
    // 使用 IndexedDB 存储离线操作
    // 当网络恢复时同步数据
    self.addEventListener('sync', event => {
      if (event.tag === 'sync-messages') {
        event.waitUntil(syncMessages());
      }
    });
    
    function syncMessages() {
      // 获取离线时存储的消息并发送到服务器
      return getMessagesFromIndexedDB()
        .then(messages => {
          return Promise.all(messages.map(msg => {
            return fetch('/api/messages', {
              method: 'POST',
              body: JSON.stringify(msg)
            }).then(() => {
              // 发送成功后从 IndexedDB 中删除
              return deleteMessageFromIndexedDB(msg.id);
            });
          }));
        });
    }

(四)如何选择适合的前端存储方案?各自有什么优缺点?

解答:

选择前端存储方案时应考虑数据特性、使用场景与安全性需求:

1. Cookies
  • 1. 优点:可设置过期时间、自动随请求发送到服务器
  • 2. 缺点:容量小(约4KB)、增加网络传输量、需小心处理安全问题
  • 3. 适用场景:身份验证令牌、跨页面追踪
2. localStorage
  • 1. 优点:永久存储、跨页面共享、容量较大(~5MB)、使用简单
  • 2. 缺点:同步操作可能阻塞UI、只能存储字符串、无法设置过期时间
  • 3. 适用场景:用户偏好设定、长期保存的应用状态
3. sessionStorage
  • 1. 优点:页面会话期间有效、页面重新加载后仍存在、相对安全
  • 2. 缺点:同步操作、只能存储字符串、不同页面无法共享
  • 3. 适用场景:表单暂存数据、单次会话的状态保存
4. IndexedDB
  • 1. 优点:大容量存储、支持索引和查询、异步操作、事务支持
  • 2. 缺点:API复杂、学习曲线陡峭、旧版浏览器支持有限
  • 3. 适用场景:大量结构化数据存储、离线应用、复杂数据操作
5. Cache API
  • 1. 优点:专为HTTP请求/响应设计、可与Service Worker结合使用
  • 2. 缺点:主要用于HTTP缓存、非通用数据存储
  • 3. 适用场景:离线应用、PWA、资源缓存

决策准则:

  • 1. 量少且需跨请求/身份验证:使用 Cookies
  • 2. 简单配置/UI状态持久保存:使用 localStorage
  • 3. 单次会话暂存数据:使用 sessionStorage
  • 4. 大量复杂数据或需要查询能力:使用 IndexedDB
  • 5. 需要离线工作或资源缓存:使用 Cache API + Service Worker

实际应用往往会组合不同存储方式:例如离线应用可能同时使用 IndexedDB(核心数据)、localStorage(用户设置)和 Cache API(资源缓存)。

(五)如何解决前端缓存更新的问题?

解答:

前端缓存更新是一个常见的挑战,有以下几种解决方案:

  1. 1. 使用版本号或哈希值
    <script src="app.3f8a7b9c.js"></script>
    
    <script src="app.js?v=1.2.3"></script>
  2. 2. 设置合适的缓存控制标头
    // HTML - 不快取或快取時間短
    Cache-Control: no-cache, must-revalidate
    
    // CSS/JS - 長時間快取,但使用版本化URL
    Cache-Control: max-age=31536000, immutable
  3. 3. Service Worker 版本管理
    // sw.js
    const CACHE_VERSION = 'v1.2.3';
    const CACHE_NAME = `app-shell-${CACHE_VERSION}`;
    
    self.addEventListener('activate', event => {
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames
              .filter(name => name.startsWith('app-shell-'))
              .filter(name => name !== CACHE_NAME)
              .map(name => caches.delete(name))
          );
        })
      );
    });
  4. 4. API 数据的刷新策略
    // SWR 策略:先显示缓存数据,然后在后台刷新
    function fetchData(url) {
      // 从缓存获取数据并立即显示
      const cachedData = localStorage.getItem(url);
      if (cachedData) {
        renderData(JSON.parse(cachedData));
      }
      
      // 同时发起新请求获取最新数据
      fetch(url)
        .then(res => res.json())
        .then(newData => {
          // 更新缓存
          localStorage.setItem(url, JSON.stringify(newData));
          // 更新显示
          renderData(newData);
        });
    }
  5. 5. 手动强制更新
    // 提供用户手动刷新功能
    function forceRefresh() {
      // 清除特定数据缓存
      localStorage.removeItem('user-data');
      
      // 或者重新加载页面,绕过缓存
      location.reload(true);
    }