H5 缓存策略:Cache First vs Network First
1. 引言
在Web应用开发中,用户体验的核心矛盾之一是 “即时性”与“可靠性”:用户希望快速访问内容(如静态资源、API数据),但网络环境(如弱网、离线状态)可能导致请求失败或延迟。H5(HTML5)通过 Service Worker 和 Cache API 提供了强大的离线缓存能力,其中 Cache First(缓存优先)和 Network First(网络优先)是两种最经典的缓存策略。
这两种策略的本质是 “如何平衡本地缓存与网络请求的优先级”:
- Cache First:优先使用本地缓存,适合 静态资源(如JS/CSS/图片) 或 对实时性要求低的场景(如品牌Logo),最大化加载速度。
- Network First:优先尝试网络请求,失败时回退到缓存,适合 动态内容(如API数据、用户信息) 或 对实时性要求高的场景(如新闻列表),确保数据的新鲜度。
本文将深入对比这两种策略的技术实现、适用场景及核心原理,并通过具体代码示例(基于Workbox或原生Service Worker)展示如何在实际项目中应用,帮助开发者根据业务需求选择最优方案。
2. 技术背景
2.1 H5缓存的核心技术
H5的缓存策略依赖以下关键技术:
- Service Worker:运行在浏览器后台的脚本(类似代理服务器),拦截网络请求(
fetch
事件),控制资源的缓存与返回逻辑。 - Cache API:浏览器提供的本地存储接口,用于存储HTTP响应(如资源文件、API数据),支持键值对形式的缓存管理(
CacheStorage
)。 - Workbox:Google开发的Service Worker工具库,封装了常见的缓存策略(如Cache First、Network First),简化开发流程。
2.2 两种策略的定义
策略 | 核心逻辑 | 典型场景 |
---|---|---|
Cache First | 1. 拦截请求后,优先查询本地缓存; 2. 若缓存命中,直接返回缓存内容; 3. 若缓存未命中,再发起网络请求,并将响应缓存供后续使用。 |
静态资源(JS/CSS/图片)、低频更新的logo |
Network First | 1. 拦截请求后,优先发起网络请求; 2. 若网络成功,返回最新响应并更新缓存; 3. 若网络失败(如超时、离线),回退到本地缓存(若有); 4. 若缓存也未命中,返回错误(如离线提示)。 |
动态内容(API数据、用户信息)、高频更新的新闻 |
2.3 为什么需要区分策略?
- 性能与实时性的权衡:
- Cache First牺牲实时性(可能返回旧内容),但大幅提升加载速度(无需等待网络往返)。
- Network First优先保证数据新鲜度(优先获取最新内容),但依赖网络稳定性(弱网时可能降级到缓存)。
- 资源类型差异:静态资源(如jQuery库)通常长期不变,适合缓存优先;动态数据(如用户订单列表)需实时更新,适合网络优先。
3. 应用使用场景
3.1 Cache First 的典型场景
- 静态资源加载:网站的JS/CSS/字体文件(如Bootstrap、Vue.js)几乎不会每日更新,优先使用缓存可减少网络请求,加速页面渲染。
- 品牌素材:网站Logo、图标等长期不变的图片,缓存后用户每次访问均快速加载,无需重复下载。
- 离线基础功能:PWA(渐进式Web应用)的核心页面(如首页)通过Cache First策略预缓存,确保用户离线时仍能打开基础界面。
3.2 Network First 的典型场景
- 用户数据展示:如个人信息页、订单列表等需要实时更新的内容,优先从网络获取最新数据,避免显示过期的缓存信息。
- 新闻/资讯流:新闻APP的文章列表需展示最新发布的内容,网络请求失败时才展示缓存的旧新闻(并提示“当前为离线模式”)。
- 表单提交状态:用户提交表单后,需通过网络请求确认是否成功,失败时回退到本地缓存的草稿或提示(而非直接依赖缓存结果)。
4. 不同场景下的详细代码实现
4.1 环境准备
- 开发工具:Chrome浏览器(支持Service Worker调试)、Node.js(可选,用于本地服务器)、Workbox(推荐,简化策略实现)或原生Service Worker代码。
- 关键API:
- Service Worker:
self.addEventListener('fetch', (event) => {})
拦截请求。 - Cache API:
caches.open('cache-name')
打开缓存,cache.match(request)
查询缓存,cache.put(request, response)
存储响应。 - Workbox:通过
workbox.strategies.CacheFirst()
或workbox.strategies.NetworkFirst()
快速配置策略。
- Service Worker:
- 注意事项:
- Service Worker需通过HTTPS(或localhost)注册,且作用域(scope)需覆盖目标资源路径。
- 缓存名称(如
'static-v1'
)建议带版本号,便于更新时清理旧缓存。
4.2 场景1:Cache First(以静态图片为例)
4.2.1 原生Service Worker实现(无Workbox)
步骤1:注册Service Worker(主线程代码,如index.html的JS)
// 主线程(如main.js)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-cache-first.js') // 注册Service Worker脚本
.then(registration => console.log('SW注册成功:', registration.scope))
.catch(err => console.error('SW注册失败:', err));
}
步骤2:编写Service Worker脚本(sw-cache-first.js)
const CACHE_NAME = 'static-cache-v1'; // 缓存名称(带版本号)
const STATIC_ASSETS = [ // 需缓存的静态资源列表(如图片、JS/CSS)
'/images/logo.png',
'/css/main.css',
'/js/app.js'
];
// 安装阶段:预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS)) // 将指定资源添加到缓存
.then(() => self.skipWaiting()) // 强制新SW立即激活
);
});
// 激活阶段:清理旧缓存(可选)
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames =>
Promise.all(
cacheNames.map(name =>
name !== CACHE_NAME && caches.delete(name) // 删除非当前版本的缓存
)
)
).then(() => self.clients.claim()) // 控制所有客户端
);
});
// 拦截请求:Cache First策略
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// 仅对静态资源应用Cache First(可根据路径过滤)
if (STATIC_ASSETS.some(asset => requestUrl.pathname === asset)) {
event.respondWith(
caches.match(event.request) // 1. 优先查询缓存
.then(cachedResponse => {
if (cachedResponse) {
console.log('Cache First: 返回缓存', event.request.url);
return cachedResponse; // 缓存命中,直接返回
}
// 2. 缓存未命中,发起网络请求并缓存响应
return fetch(event.request).then(networkResponse => {
if (!networkResponse || networkResponse.status !== 200) {
return networkResponse; // 网络请求失败,返回原始响应(可能为错误)
}
// 克隆响应(因响应流只能读取一次)
const clonedResponse = networkResponse.clone();
caches.open(CACHE_NAME).then(cache =>
cache.put(event.request, clonedResponse) // 缓存网络响应
);
return networkResponse;
});
})
);
}
});
4.2.2 原理解释
- 安装阶段(install):Service Worker安装时,通过
cache.addAll()
预缓存指定的静态资源(如logo.png),确保这些资源在首次访问时即可离线使用。 - 拦截请求(fetch):当浏览器请求静态资源时,Service Worker优先通过
caches.match()
查询本地缓存:- 若缓存命中(如用户第二次访问logo.png),直接返回缓存的响应(无需网络请求)。
- 若缓存未命中(如首次访问或缓存被清理),发起网络请求获取资源,并将响应克隆后存入缓存(供后续使用)。
- 优势:静态资源的加载完全依赖本地缓存,速度极快(毫秒级),适合对实时性无要求的场景。
4.3 场景2:Network First(以API数据为例)
4.3.1 原生Service Worker实现
步骤1:编写Service Worker脚本(sw-network-first.js)
const CACHE_NAME = 'api-cache-v1';
const API_ENDPOINTS = ['/api/user-info', '/api/news']; // 需网络优先的API路径
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
const isApiRequest = API_ENDPOINTS.some(endpoint => requestUrl.pathname.startsWith(endpoint));
if (isApiRequest) {
event.respondWith(
// 1. 优先发起网络请求
fetch(event.request)
.then(networkResponse => {
if (!networkResponse || networkResponse.status !== 200) {
throw new Error('网络请求失败'); // 网络失败时抛出异常
}
// 克隆响应(缓存和返回各需一份)
const clonedResponse = networkResponse.clone();
// 2. 网络成功,更新缓存并返回最新数据
caches.open(CACHE_NAME).then(cache =>
cache.put(event.request, clonedResponse)
);
return networkResponse;
})
.catch(() => {
// 3. 网络失败,回退到缓存
return caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
console.log('Network First: 网络失败,返回缓存', event.request.url);
return cachedResponse; // 返回缓存的旧数据(并提示用户“当前为离线数据”)
}
// 4. 缓存也未命中,返回离线提示(或原始错误)
return new Response(JSON.stringify({ error: '离线且无缓存数据' }), {
headers: { 'Content-Type': 'application/json' },
status: 408
});
});
})
);
}
});
步骤2:注册Service Worker(同Cache First场景)
4.3.2 原理解释
- 拦截API请求:当浏览器请求
/api/user-info
或/api/news
时,Service Worker优先发起网络请求(fetch(event.request)
)。 - 网络优先逻辑:
- 若网络请求成功(状态码200),克隆响应并存储到缓存(
cache.put
),然后返回最新数据给页面。 - 若网络请求失败(如超时、离线),通过
caches.match()
查询本地缓存:- 若缓存存在(如用户上次在线时获取的用户信息),返回缓存的旧数据(并可通过UI提示“当前为离线模式”)。
- 若缓存不存在,返回自定义的离线错误(如
{ error: '离线且无缓存数据' }
)。
- 若网络请求成功(状态码200),克隆响应并存储到缓存(
- 优势:动态数据始终优先获取最新内容,仅在网络不可用时降级到缓存,平衡了实时性与可靠性。
5. 原理解释
5.1 核心流程对比
Cache First 原理流程图
sequenceDiagram
participant User as 用户
participant Browser as 浏览器
participant SW as Service Worker
participant Cache as 本地缓存
participant Network as 网络
User->>Browser: 请求静态资源(如logo.png)
Browser->>SW: 拦截fetch事件
SW->>Cache: 查询缓存(match)
alt 缓存命中
Cache-->>SW: 返回缓存响应
SW-->>Browser: 直接返回缓存
Browser-->>User: 显示资源(快速加载)
else 缓存未命中
SW->>Network: 发起网络请求
Network-->>SW: 返回资源响应
SW->>Cache: 存储响应(put)
SW-->>Browser: 返回网络响应
Browser-->>User: 显示资源(首次加载稍慢)
end
Network First 原理流程图
sequenceDiagram
participant User as 用户
participant Browser as 浏览器
participant SW as Service Worker
participant Network as 网络
participant Cache as 本地缓存
User->>Browser: 请求API数据(如/user-info)
Browser->>SW: 拦截fetch事件
SW->>Network: 优先发起网络请求
alt 网络成功
Network-->>SW: 返回最新响应
SW->>Cache: 存储响应(put)
SW-->>Browser: 返回最新响应
Browser-->>User: 显示最新数据
else 网络失败
SW->>Cache: 回退查询缓存(match)
alt 缓存命中
Cache-->>SW: 返回缓存响应
SW-->>Browser: 返回缓存数据
Browser-->>User: 显示旧数据(提示离线)
else 缓存未命中
SW-->>Browser: 返回离线错误
Browser-->>User: 显示“无数据”提示
end
end
5.2 核心特性总结
特性 | Cache First | Network First |
---|---|---|
优先级 | 本地缓存 > 网络请求 | 网络请求 > 本地缓存 |
实时性 | 低(可能返回旧内容) | 高(优先获取最新内容) |
可靠性 | 高(离线时仍可使用缓存) | 中(网络失败时依赖缓存) |
适用资源 | 静态资源(JS/CSS/图片)、低频更新内容 | 动态数据(API、用户信息)、高频更新内容 |
性能影响 | 极大提升加载速度(无网络延迟) | 可能因网络延迟导致加载变慢(但优先最新) |
6. 原生实现 vs Workbox实现
上述代码为原生Service Worker实现,实际开发中推荐使用 Workbox(Google官方库),可大幅简化代码。例如,Cache First策略仅需一行配置:
// 使用Workbox的Cache First策略(sw-workbox.js)
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// 静态资源:Cache First + 缓存过期控制
registerRoute(
({ request }) => request.destination === 'image' || request.url.includes('/static/'),
new CacheFirst({
cacheName: 'static-cache',
plugins: [new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 })] // 最多50条,缓存30天
})
);
// API数据:Network First
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 3 // 网络超时3秒后回退缓存
})
);
7. 实际详细应用代码示例实现(综合案例:电商H5页面)
7.1 需求描述
电商H5页面包含:
- 静态部分:商品列表页的CSS/JS(长期不变)、品牌Logo(长期不变)→ 使用Cache First。
- 动态部分:商品详情API(价格/库存实时更新)、用户购物车数据 → 使用Network First。
7.2 代码实现(Workbox版)
步骤1:注册Service Worker(main.js)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-ecommerce.js')
.then(() => console.log('电商SW注册成功'))
.catch(err => console.error('注册失败:', err));
}
步骤2:编写Service Worker(sw-ecommerce.js,基于Workbox)
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// 静态资源:Cache First(CSS/JS/Logo)
registerRoute(
({ request }) => request.destination === 'script' ||
request.destination === 'style' ||
request.url.includes('/logo.png'),
new CacheFirst({
cacheName: 'static-assets',
plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 })] // 缓存30天
})
);
// API数据:Network First(商品详情/购物车)
registerRoute(
({ url }) => url.pathname.startsWith('/api/product/') || url.pathname.startsWith('/api/cart/'),
new NetworkFirst({
cacheName: 'api-data',
networkTimeoutSeconds: 2, // 网络超时2秒后回退缓存
plugins: [new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 })] // 缓存1小时
})
);
8. 运行结果
- Cache First场景:用户首次访问电商页面时,Logo和CSS/JS从网络加载并缓存;后续访问直接从缓存读取,页面秒开(即使离线也能显示基础内容)。
- Network First场景:用户查看商品详情时,优先获取最新的价格和库存(网络请求);若在地铁弱网环境下失败,则显示上次缓存的旧数据(并提示“当前为离线模式”)。
9. 测试步骤及详细代码
9.1 测试Cache First
- 首次访问:清除浏览器缓存,访问电商页面,观察Network面板确认Logo/CSS/JS从网络加载。
- 二次访问:刷新页面,确认相同资源从“disk cache”或“memory cache”加载(无网络请求)。
- 离线测试:关闭网络,刷新页面,确认静态资源仍能显示(Logo/CSS正常),动态API返回离线错误。
9.2 测试Network First
- 正常网络:访问商品详情页,确认数据为最新(如价格更新)。
- 弱网模拟:通过Chrome DevTools的“Network Throttling”设置为“Slow 3G”,或关闭网络,观察是否回退到缓存的旧数据。
- 缓存更新:恢复网络后再次访问,确认数据更新为最新版本(缓存被新响应覆盖)。
测试代码(模拟离线):
// 在DevTools Console中手动模拟离线
navigator.serviceWorker.ready.then(registration => {
registration.active.postMessage({ type: 'FORCE_OFFLINE' }); // 可通过SW监听此消息强制离线逻辑
});
10. 部署场景
- PWA应用:电商、新闻类H5页面通过Cache First预缓存核心资源,Network First保证动态内容实时性,实现“离线可用+在线新鲜”。
- 企业内部门户:内部系统的基础框架(如导航栏)用Cache First加速加载,业务数据(如报表)用Network First确保准确性。
- 跨平台Hybrid App:WebView内嵌的H5页面通过Service Worker缓存策略,减少对原生网络的依赖,提升混合应用的响应速度。
11. 疑难解答
-
Q1:缓存未生效?
A1:检查Service Worker是否注册成功(Console输出“注册成功”),确认请求URL匹配策略中的过滤条件(如路径/api/
),并确保资源通过HTTPS访问(本地可用localhost)。 -
Q2:Network First为何不回退缓存?
A2:可能是缓存中无对应请求的响应(如首次访问API且网络失败),或缓存名称/路径配置错误(检查registerRoute
的URL匹配规则)。 -
Q3:如何清理旧缓存?
A3:在Service Worker的activate
事件中通过caches.delete('old-cache-name')
清理,或使用Workbox的ExpirationPlugin
自动过期。
12. 未来展望
- 智能策略切换:结合用户网络状态(如通过
navigator.connection.effectiveType
检测4G/WiFi),动态调整Cache First/Network First策略(如WiFi下优先网络,4G下优先缓存)。 - 边缘缓存协同:与CDN边缘计算结合,将部分缓存逻辑下沉到边缘节点,进一步降低延迟。
- AI驱动的缓存预测:通过机器学习分析用户行为(如常访问的商品页),预缓存可能需要的资源,提升命中率。
13. 技术趋势与挑战
- 趋势:
- Service Worker标准化:更多浏览器支持高级缓存API(如Background Fetch),增强离线能力。
- 缓存与SSR结合:服务端渲染(SSR)的首屏内容与客户端缓存策略协同,平衡首屏速度与SEO。
- 挑战:
- 缓存一致性:动态内容的缓存更新时机难以精确控制(如商品价格秒级变化),需设计合理的缓存失效机制(如版本号、时间戳)。
- 安全风险:恶意缓存(如篡改的JS文件)可能导致XSS攻击,需严格校验缓存内容的完整性(如Subresource Integrity)。
14. 总结
Cache First和Network First是H5缓存策略的两大基石,分别针对 静态资源的高性能加载 和 动态数据的实时性需求 设计。通过Service Worker和Cache API,开发者可以灵活控制资源的缓存逻辑,结合Workbox等工具库,显著提升Web应用的离线能力、加载速度与用户体验。未来,随着边缘计算与AI技术的融合,缓存策略将更加智能化,成为Web应用“快、稳、智”的核心竞争力。开发者应根据具体业务场景(如资源类型、实时性要求)选择合适的策略,并通过持续测试与优化,打造极致的用户体验。
- 点赞
- 收藏
- 关注作者
评论(0)