React 做"离线缓存 + 图片缓存",建议不要把它当成一个单一缓存问题,而是拆成三层:
-
API / JSON 数据缓存
-
图片资源缓存
-
整个页面 / App 的离线能力
一个比较成熟的方案是:
React
├─ React Query / TanStack Query
│ └─ API 数据缓存、失效、后台刷新
│
├─ Service Worker
│ ├─ API 离线兜底
│ └─ 静态资源缓存
│
└─ 图片
├─ HTTP Cache
├─ Service Worker Cache
└─ IndexedDB(只在特殊场景使用)
一、API 数据缓存:优先 TanStack Query
如果项目是 React,推荐直接使用 TanStack Query,而不是自己写 localStorage 缓存。
例如:
const { data, isLoading } = useQuery({
queryKey: ['products', categoryId],
queryFn: () => fetchProducts(categoryId),
staleTime: 5 * 60 * 1000,
gcTime: 24 * 60 * 60 * 1000,
});
它解决的是:
-
内存缓存
-
请求去重
-
stale-while-revalidate
-
后台刷新
-
请求失败重试
-
cache invalidation
如果要求刷新页面后仍然有缓存,可以持久化 Query Cache:
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
不过,大量数据不建议放 localStorage,因为容量和同步 API 都比较弱。生产项目更推荐 IndexedDB。
二、图片缓存:优先 HTTP Cache
很多时候其实不需要自己实现图片缓存。
服务器正确配置:
Cache-Control: public, max-age=31536000, immutable
然后图片 URL 使用版本化:
/avatar/user-123-v3.webp
这样浏览器本身就是最有效的图片缓存层。
React:
<img
src="https://cdn.example.com/images/a8f3c.webp"
loading="lazy"
decoding="async"
/>
推荐:
CDN
↓
HTTP Cache
↓
Browser Disk Cache
这比把所有图片主动塞进 IndexedDB 通常更简单。
三、需要"离线还能看到图片":Service Worker
这是离线图片缓存最值得采用的方案。
例如:
第一次访问
↓
请求图片
↓
Service Worker
↓
网络成功
↓
写入 Cache Storage
以后:
请求图片
↓
Service Worker
↓
Cache 命中 → 直接返回
│
└─ 未命中 → 请求网络 → 缓存 → 返回
简单示例:
self.addEventListener('fetch', event => {
const request = event.request
if (request.destination === 'image') {
event.respondWith(
caches.match(request).then(cached => {
if (cached) {
return cached
}
return fetch(request).then(response => {
const clone = response.clone()
caches.open('images-v1').then(cache => {
cache.put(request, clone)
})
return response
})
})
)
}
})
不过生产环境不要直接照这个写,还要考虑:
-
cache size
-
LRU 淘汰
-
CDN URL 参数
-
opaque response
-
图片版本更新
-
缓存过期
-
storage quota
-
跨域
四、Cache Storage 和 IndexedDB 怎么选?
这是一个比较重要的区别:
| 场景 | 推荐 |
|---|---|
| API JSON | TanStack Query |
| API 离线数据 | IndexedDB + TanStack Query Persist |
| CSS / JS / HTML | Cache Storage |
| 图片 | HTTP Cache / Cache Storage |
| 大量图片二进制 | 通常 Cache Storage |
| 图片 Blob + 自定义 metadata | IndexedDB |
| 需要搜索、排序、复杂离线数据库 | IndexedDB |
不要默认把图片全部放 IndexedDB。
浏览器本身的 Cache Storage 对 Request / Response 设计得更自然。
五、如果你要做 PWA
比较完整的架构可以是:
┌───────────────┐
│ React UI │
└───────┬───────┘
│
┌─────────────┴─────────────┐
│ │
TanStack Query Image <img>
│ │
Query Cache HTTP Cache
│ │
IndexedDB Service Worker
│ │
└─────────────┬─────────────┘
│
Cache Storage
│
Network
六、推荐直接使用 Workbox
生产项目不太建议手撸完整 Service Worker,Workbox 会舒服很多。
例如 Vite:
npm install vite-plugin-pwa
配置:
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|webp|svg)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 200,
maxAgeSeconds: 60 * 60 * 24 * 30,
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
}),
],
})
这个方案已经可以覆盖大多数 React Web App。
七、图片缓存策略怎么选?
主要有三个:
CacheFirst
Cache → Network
适合:
-
用户头像
-
商品图片
-
Banner
-
静态图片
优点是速度最快。
NetworkFirst
Network → Cache
适合:
-
经常变化的图片
-
用户上传后的资源
-
需要尽量保证最新版本的资源
StaleWhileRevalidate
Cache → 立即返回
↓
后台请求最新图片
↓
更新 Cache
非常适合:
新闻图片
Feed 图片
商品列表
首页推荐
用户体验通常很好。
八、不要用 localStorage 存图片
不建议这样:
localStorage.setItem(
'image',
base64
)
原因:
-
容量小
-
Base64 会膨胀
-
同步 API
-
主线程开销
-
不适合大量二进制
图片缓存优先:
HTTP Cache
↓
Service Worker / Cache Storage
↓
IndexedDB(特殊需求)
九、我比较推荐的实际技术栈
如果是一个正常的 React + Vite 项目:
React
├── TanStack Query
├── IndexedDB
│ └── Dexie
├── vite-plugin-pwa
├── Workbox
└── CDN
对应关系:
接口数据
→ TanStack Query
接口离线持久化
→ IndexedDB / Dexie
JS/CSS/HTML
→ Service Worker
图片
→ CDN + HTTP Cache
→ Workbox CacheFirst / StaleWhileRevalidate
整个 App 离线
→ PWA + Service Worker
如果你的应用是后台管理系统 / 电商 / 社交 Feed / 移动端 H5 中的某一种,我还可以进一步给你一套针对该场景的缓存架构 + React/Workbox 完整代码结构。