前言
现在的开发应用,都在强调用户体验至上,页面加载速度直接影响着用户留存和转化率。据统计,页面加载时间从1秒增加到3秒,跳出率提升32%;从1秒增加到5秒,跳出率提升90%。而缓存,正是提升应用性能最有效的手段之一。
然而,缓存也是一把双刃剑:用得好,应用会快如闪电;用得不好,数据不一致、内存泄漏等问题接踵而至。本文将深入探讨前端缓存的多种形式、实现策略和最佳实践,帮你构建高性能、高可靠性的应用。
为什么需要前端缓存?
减少网络请求,提升加载速度
网络请求是前端性能的最大瓶颈。每次请求都要经过 DNS 解析、TCP 连接、SSL 握手、请求响应等环节,耗时动辄几百毫秒甚至几秒。
typescript
// ❌ 没有缓存:每次都要请求
async function getUserInfo(userId: number) {
return await api.get(`/users/${userId}`)
}
// ✅ 有缓存:重复请求直接返回
const userCache = new Map<number, User>()
async function getUserInfoWithCache(userId: number) {
if (userCache.has(userId)) {
return userCache.get(userId)
}
const user = await api.get(`/users/${userId}`)
userCache.set(userId, user)
return user
}
离线访问能力
通过 Service Worker 和 Cache API,我们可以让应用在无网络环境下也能正常运行:
javascript
// service-worker.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 有缓存返回缓存
if (response) {
return response
}
// 无缓存请求网络,并存入缓存
return fetch(event.request).then((response) => {
const responseClone = response.clone()
caches.open('v1').then((cache) => {
cache.put(event.request, responseClone)
})
return response
})
})
)
})
减轻服务器压力
高并发场景下,缓存能有效降低服务器负载。假设一个接口每秒有1000次请求,如果缓存命中率90%,服务器实际只需要处理100次请求。
text
服务器压力对比
无缓存: 1000 QPS -> 需要10台服务器
有缓存: 100 QPS -> 只需要1台服务器
前端缓存的多种形式
HTTP 缓存:强缓存、协商缓存
强缓存:浏览器直接读取本地缓存,不发请求
bash
# 响应头示例
Cache-Control: max-age=3600 # 缓存1小时
Expires: Wed, 21 Oct 2025 07:28:00 GMT
协商缓存:发送请求,由服务器决定是否使用缓存
bash
# 请求头
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 响应头
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Vite 构建产物配置
javascript
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
// 给静态资源添加 hash,实现永久缓存
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]'
}
}
}
}
Service Worker 缓存:PWA、离线可用
Service Worker 作为浏览器和网络之间的代理,可以实现精细化的缓存控制:
javascript
// service-worker.js
const CACHE_NAME = 'my-app-v1'
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
]
// 安装阶段:预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache)
})
)
})
// 拦截请求:实现不同缓存策略
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// 策略1:Cache First(静态资源)
if (url.pathname.match(/\.(css|js|png|jpg)$/)) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
}
// 策略2:Network First(API 请求)
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone)
})
return response
})
.catch(() => caches.match(event.request))
)
}
// 策略3:Stale While Revalidate(数据更新不频繁)
// 先返回缓存,同时发起请求更新缓存
})
内存缓存:Map/WeakMap、ref 存储
内存缓存最快,但数据会随页面刷新而消失:
typescript
// 简单的 Map 缓存
const tokenCache = new Map<string, string>()
export function getToken(userId: string) {
return tokenCache.get(userId)
}
export function setToken(userId: string, token: string) {
tokenCache.set(userId, token)
// 设置过期时间
setTimeout(() => {
tokenCache.delete(userId)
}, 3600000) // 1小时后过期
}
WeakMap 的优势:不会阻止垃圾回收,适合缓存 DOM 相关数据
typescript
// WeakMap 缓存 DOM 数据
const domDataCache = new WeakMap<HTMLElement, any>()
export function getDomData(el: HTMLElement) {
return domDataCache.get(el)
}
export function setDomData(el: HTMLElement, data: any) {
domDataCache.set(el, data)
}
持久化缓存:localStorage、sessionStorage、IndexedDB
| 存储方式 | 容量 | 同步/异步 | 数据类型 | 生命周期 |
|---|---|---|---|---|
| localStorage | 5-10MB | 同步 | 字符串 | 永久 |
| sessionStorage | 5-10MB | 同步 | 字符串 | 标签页关闭 |
| IndexedDB | 数百MB | 异步 | 结构化数据 | 永久 |
内存缓存的实现
简单场景:字典数据缓存
后台管理系统中,字典数据(性别、状态、类型等)通常不会频繁变化,非常适合缓存:
typescript
// useDictionary.ts
import { ref } from 'vue'
import type { Ref } from 'vue'
interface DictionaryItem {
value: string | number
label: string
[key: string]: any
}
interface CacheItem {
data: DictionaryItem[]
expire: number
}
class DictionaryCache {
private cache = new Map<string, CacheItem>()
private pendingRequests = new Map<string, Promise<DictionaryItem[]>>()
// 获取字典
async get(type: string, forceRefresh = false): Promise<DictionaryItem[]> {
// 强制刷新:忽略缓存
if (forceRefresh) {
return this.fetchAndCache(type)
}
// 检查缓存
const cached = this.cache.get(type)
if (cached && cached.expire > Date.now()) {
return cached.data
}
// 避免并发重复请求
if (this.pendingRequests.has(type)) {
return this.pendingRequests.get(type)!
}
// 发起请求
const promise = this.fetchAndCache(type)
this.pendingRequests.set(type, promise)
try {
return await promise
} finally {
this.pendingRequests.delete(type)
}
}
// 预加载字典
preload(types: string[]) {
types.forEach(type => {
this.get(type).catch(() => {
console.warn(`预加载字典失败: ${type}`)
})
})
}
// 清空缓存
clear() {
this.cache.clear()
}
private async fetchAndCache(type: string): Promise<DictionaryItem[]> {
try {
const data = await api.getDictionary(type)
this.cache.set(type, {
data,
expire: Date.now() + 3600000 // 1小时过期
})
return data
} catch (error) {
// 如果有过期数据,降级使用
const expired = this.cache.get(type)
if (expired) {
console.warn(`使用过期字典数据: ${type}`)
return expired.data
}
throw error
}
}
}
export const dictCache = new DictionaryCache()
// 在组件中使用
import { dictCache } from './cache/dictionary'
export function useDictionary(type: string) {
const data = ref<DictionaryItem[]>([])
const loading = ref(true)
const error = ref<Error | null>(null)
dictCache.get(type)
.then(items => {
data.value = items
})
.catch(e => {
error.value = e
})
.finally(() => {
loading.value = false
})
return { data, loading, error }
}
带过期时间的缓存
typescript
// utils/timed-cache.ts
interface TimedCacheItem<T> {
data: T
expire: number
createdAt: number
hitCount: number
}
class TimedCache<K, V> {
private cache = new Map<K, TimedCacheItem<V>>()
private defaultTTL: number // 毫秒
constructor(defaultTTL = 60000) {
this.defaultTTL = defaultTTL
}
set(key: K, value: V, ttl?: number) {
this.cache.set(key, {
data: value,
expire: Date.now() + (ttl || this.defaultTTL),
createdAt: Date.now(),
hitCount: 0
})
}
get(key: K): V | null {
const item = this.cache.get(key)
if (!item) return null
// 检查是否过期
if (item.expire < Date.now()) {
this.cache.delete(key)
return null
}
// 更新命中次数
item.hitCount++
return item.data
}
has(key: K): boolean {
const item = this.cache.get(key)
if (!item) return false
if (item.expire < Date.now()) {
this.cache.delete(key)
return false
}
return true
}
delete(key: K) {
this.cache.delete(key)
}
clear() {
this.cache.clear()
}
// 清理过期缓存
cleanup() {
const now = Date.now()
this.cache.forEach((item, key) => {
if (item.expire < now) {
this.cache.delete(key)
}
})
}
// 获取缓存统计
getStats() {
const stats = {
size: this.cache.size,
totalHits: 0,
oldest: 0,
newest: 0
}
let now = Date.now()
this.cache.forEach(item => {
stats.totalHits += item.hitCount
stats.oldest = Math.min(stats.oldest || item.createdAt, item.createdAt)
stats.newest = Math.max(stats.newest, item.createdAt)
})
return stats
}
}
export const timedCache = new TimedCache<string, any>()
LRU 淘汰策略的实现
当缓存数量超过限制时,淘汰最久未使用的数据:
typescript
// utils/lru-cache.ts
interface ListNode<K, V> {
key: K
value: V
prev: ListNode<K, V> | null
next: ListNode<K, V> | null
}
class LRUCache<K, V> {
private capacity: number
private cache = new Map<K, ListNode<K, V>>()
private head: ListNode<K, V> | null = null
private tail: ListNode<K, V> | null = null
constructor(capacity: number) {
this.capacity = capacity
}
get(key: K): V | null {
const node = this.cache.get(key)
if (!node) return null
// 移动到链表头部(表示最近使用)
this.moveToHead(node)
return node.value
}
put(key: K, value: V) {
if (this.cache.has(key)) {
// 更新现有节点
const node = this.cache.get(key)!
node.value = value
this.moveToHead(node)
} else {
// 创建新节点
const node: ListNode<K, V> = {
key,
value,
prev: null,
next: null
}
// 如果缓存已满,淘汰尾部节点
if (this.cache.size >= this.capacity) {
this.removeTail()
}
// 添加到头部
this.addToHead(node)
this.cache.set(key, node)
}
}
private moveToHead(node: ListNode<K, V>) {
if (node === this.head) return
// 从当前位置移除
if (node.prev) {
node.prev.next = node.next
}
if (node.next) {
node.next.prev = node.prev
}
if (node === this.tail) {
this.tail = node.prev
}
// 添加到头部
node.prev = null
node.next = this.head
if (this.head) {
this.head.prev = node
}
this.head = node
if (!this.tail) {
this.tail = node
}
}
private addToHead(node: ListNode<K, V>) {
node.prev = null
node.next = this.head
if (this.head) {
this.head.prev = node
}
this.head = node
if (!this.tail) {
this.tail = node
}
}
private removeTail() {
if (!this.tail) return
this.cache.delete(this.tail.key)
if (this.tail === this.head) {
this.head = null
this.tail = null
} else {
this.tail = this.tail.prev
if (this.tail) {
this.tail.next = null
}
}
}
clear() {
this.cache.clear()
this.head = null
this.tail = null
}
get size() {
return this.cache.size
}
}
export const lruCache = new LRUCache<string, any>(100)
IndexedDB 大规模数据缓存
何时需要 IndexedDB?
IndexedDB 适合存储大量结构化数据,常见场景:
- 离线应用数据:PWA 应用的核心数据
- 用户操作日志:批量上报前暂存
- 大屏历史数据:历史趋势数据
- 富文本编辑器内容:自动保存草稿
- 音视频文件:分段缓存
使用 idb-keyval 简化操作
原生 IndexedDB API 较为复杂,推荐使用 idb-keyval 简化操作:
bash
npm install idb-keyval
typescript
// utils/idb-cache.ts
import { get, set, del, clear, keys, createStore } from 'idb-keyval'
// 创建不同的存储空间
const userStore = createStore('user-db', 'user-store')
const logStore = createStore('log-db', 'log-store')
const fileStore = createStore('file-db', 'file-store')
export class IDBCache {
private store: IDBObjectStore
constructor(dbName: string, storeName: string) {
this.store = createStore(dbName, storeName)
}
// 存储数据
async set<T>(key: string, value: T): Promise<void> {
await set(key, value, this.store)
}
// 获取数据
async get<T>(key: string): Promise<T | undefined> {
return await get<T>(key, this.store)
}
// 删除数据
async delete(key: string): Promise<void> {
await del(key, this.store)
}
// 清空存储
async clear(): Promise<void> {
await clear(this.store)
}
// 获取所有键
async keys(): Promise<string[]> {
return await keys<string>(this.store)
}
// 批量存储
async setMany(entries: [string, any][]): Promise<void> {
const promises = entries.map(([key, value]) =>
set(key, value, this.store)
)
await Promise.all(promises)
}
// 批量获取
async getMany<T>(keys: string[]): Promise<(T | undefined)[]> {
const promises = keys.map(key => get<T>(key, this.store))
return await Promise.all(promises)
}
}
// 创建缓存实例
export const userCache = new IDBCache('my-app', 'users')
export const logCache = new IDBCache('my-app', 'logs')
export const fileCache = new IDBCache('my-app', 'files')
实战:缓存用户操作日志
typescript
// services/log.service.ts
import { logCache } from './idb-cache'
interface LogEntry {
id: string
type: 'click' | 'view' | 'error' | 'api'
timestamp: number
data: any
userId?: string
}
class LogService {
private batchSize = 20
private uploadInterval = 30000 // 30秒
private timer: ReturnType<typeof setTimeout> | null = null
constructor() {
// 定时上传日志
this.startAutoUpload()
// 页面关闭前尝试上传
window.addEventListener('beforeunload', () => {
this.uploadNow()
})
}
// 记录日志
async log(log: Omit<LogEntry, 'id' | 'timestamp'>) {
const entry: LogEntry = {
id: this.generateId(),
timestamp: Date.now(),
...log
}
// 先存到 IndexedDB
await this.saveToIndexedDB(entry)
// 检查是否需要立即上传
await this.checkBatchUpload()
}
private async saveToIndexedDB(entry: LogEntry) {
const key = `log_${entry.timestamp}_${entry.id}`
await logCache.set(key, entry)
}
private async checkBatchUpload() {
const keys = await logCache.keys()
if (keys.length >= this.batchSize) {
await this.uploadNow()
}
}
private startAutoUpload() {
this.timer = setInterval(() => {
this.uploadNow()
}, this.uploadInterval)
}
// 立即上传所有日志
async uploadNow() {
if (this.timer) {
clearTimeout(this.timer)
}
const keys = await logCache.keys()
if (keys.length === 0) return
const logs = await logCache.getMany<LogEntry>(keys)
const validLogs = logs.filter(log => log !== undefined) as LogEntry[]
try {
// 上传到服务器
await api.uploadLogs(validLogs)
// 上传成功,删除本地日志
await Promise.all(keys.map(key => logCache.delete(key)))
console.log(`上传 ${validLogs.length} 条日志成功`)
} catch (error) {
console.error('上传日志失败', error)
// 重新启动定时器
this.startAutoUpload()
}
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
}
export const logService = new LogService()
// 在组件中使用
import { logService } from './services/log.service'
// 记录用户操作
function handleButtonClick() {
logService.log({
type: 'click',
data: { buttonId: 'submit', page: '/checkout' },
userId: userStore.user?.id
})
}
缓存更新策略
写穿策略:同时更新缓存和后端
typescript
// services/user.service.ts
class UserService {
private cache = new Map<number, User>()
// 获取用户(先读缓存)
async getUser(id: number): Promise<User> {
if (this.cache.has(id)) {
return this.cache.get(id)!
}
const user = await api.getUser(id)
this.cache.set(id, user)
return user
}
// 更新用户(写穿策略)
async updateUser(id: number, data: Partial<User>): Promise<User> {
// 1. 先更新后端
const updated = await api.updateUser(id, data)
// 2. 更新缓存
this.cache.set(id, updated)
return updated
}
}
失效策略:更新后端后清除缓存
typescript
// services/product.service.ts
class ProductService {
private cache = new Map<number, Product>()
// 获取产品
async getProduct(id: number): Promise<Product> {
if (this.cache.has(id)) {
return this.cache.get(id)!
}
const product = await api.getProduct(id)
this.cache.set(id, product)
return product
}
// 更新产品(失效策略)
async updateProduct(id: number, data: Partial<Product>): Promise<Product> {
// 1. 更新后端
const updated = await api.updateProduct(id, data)
// 2. 删除缓存(下次读取时重新获取)
this.cache.delete(id)
return updated
}
// 批量失效
invalidateProduct(id: number) {
this.cache.delete(id)
}
invalidateAll() {
this.cache.clear()
}
}
定时刷新:轮询更新缓存数据
typescript
// composables/usePollingCache.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function usePollingCache<T>(
key: string,
fetcher: () => Promise<T>,
options: {
interval?: number // 轮询间隔
retryOnError?: boolean // 失败是否重试
maxRetries?: number // 最大重试次数
} = {}
) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const {
interval = 30000,
retryOnError = true,
maxRetries = 3
} = options
let timer: ReturnType<typeof setTimeout> | null = null
let retryCount = 0
// 获取数据
const fetchData = async () => {
loading.value = true
error.value = null
try {
const result = await fetcher()
data.value = result
retryCount = 0 // 成功重置重试计数
} catch (err) {
error.value = err as Error
// 失败重试逻辑
if (retryOnError && retryCount < maxRetries) {
retryCount++
console.log(`第${retryCount}次重试...`)
setTimeout(fetchData, interval / 2)
}
} finally {
loading.value = false
}
}
// 开始轮询
const startPolling = () => {
if (timer) return
fetchData()
timer = setInterval(fetchData, interval)
}
// 停止轮询
const stopPolling = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
// 手动刷新
const refresh = () => {
stopPolling()
startPolling()
}
onMounted(() => {
startPolling()
})
onUnmounted(() => {
stopPolling()
})
return {
data,
loading,
error,
refresh,
stopPolling,
startPolling
}
}
// 使用
const { data, loading } = usePollingCache(
'stock-data',
() => api.getStockPrices(),
{ interval: 10000 }
)
WebSocket 推送:服务端通知缓存失效
typescript
// services/realtime-cache.ts
import { ref } from 'vue'
class RealtimeCache {
private cache = new Map<string, any>()
private ws: WebSocket | null = null
constructor() {
this.connectWebSocket()
}
private connectWebSocket() {
this.ws = new WebSocket('wss://api.example.com/realtime')
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'invalidate':
// 失效特定缓存
this.invalidate(message.key)
break
case 'update':
// 直接更新缓存
this.set(message.key, message.data)
break
case 'invalidate-all':
// 失效所有缓存
this.cache.clear()
break
}
}
this.ws.onclose = () => {
// 断线重连
setTimeout(() => this.connectWebSocket(), 5000)
}
}
get(key: string) {
return this.cache.get(key)
}
set(key: string, value: any) {
this.cache.set(key, value)
}
invalidate(key: string) {
this.cache.delete(key)
}
// 获取数据,优先从缓存读,同时监听失效
async getData<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const cached = this.get(key)
if (cached) {
return cached
}
const data = await fetcher()
this.set(key, data)
return data
}
}
export const realtimeCache = new RealtimeCache()
// 在组件中使用
const user = await realtimeCache.getData(
`user_${userId}`,
() => api.getUser(userId)
)
结合 Vue 的响应式缓存
useSWR 风格的缓存
**SWR (Stale-While-Revalidate) ** 策略:先返回缓存数据,同时发起请求更新缓存:
typescript
// composables/useSWR.ts
import { ref, watch, onUnmounted, type Ref } from 'vue'
interface CacheEntry<T> {
data: T
timestamp: number
promise?: Promise<T>
}
class SWRCache {
private cache = new Map<string, CacheEntry<any>>()
private subscribers = new Map<string, Set<(data: any) => void>>()
get<T>(key: string): T | null {
const entry = this.cache.get(key)
if (entry && Date.now() - entry.timestamp < 60000) { // 1分钟内有效
return entry.data
}
return null
}
set<T>(key: string, data: T) {
this.cache.set(key, {
data,
timestamp: Date.now()
})
// 通知所有订阅者
const subscribers = this.subscribers.get(key)
if (subscribers) {
subscribers.forEach(cb => cb(data))
}
}
subscribe(key: string, callback: (data: any) => void) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set())
}
this.subscribers.get(key)!.add(callback)
return () => {
this.subscribers.get(key)?.delete(callback)
}
}
clear() {
this.cache.clear()
this.subscribers.clear()
}
}
const globalCache = new SWRCache()
export function useSWR<T>(
key: string,
fetcher: () => Promise<T>,
options: {
revalidateOnMount?: boolean
revalidateOnFocus?: boolean
refreshInterval?: number
dedupingInterval?: number // 去重间隔
} = {}
) {
const data = ref<T | null>(globalCache.get(key)) as Ref<T | null>
const isLoading = ref(!data.value)
const error = ref<Error | null>(null)
let timer: ReturnType<typeof setTimeout> | null = null
let revalidateTimer: ReturnType<typeof setTimeout> | null = null
const {
revalidateOnMount = true,
revalidateOnFocus = true,
refreshInterval = 0,
dedupingInterval = 2000
} = options
// 重新验证数据
const revalidate = async () => {
const entry = globalCache['cache'].get(key)
// 去重:短时间内不重复请求
if (entry?.promise) {
return entry.promise
}
isLoading.value = true
error.value = null
const promise = fetcher()
.then((newData) => {
globalCache.set(key, newData)
data.value = newData
return newData
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
isLoading.value = false
// 清除 promise 引用
setTimeout(() => {
const entry = globalCache['cache'].get(key)
if (entry?.promise === promise) {
entry.promise = undefined
}
}, dedupingInterval)
})
const entry = globalCache['cache'].get(key)
if (entry) {
entry.promise = promise
}
return promise
}
// 订阅缓存更新
const unsubscribe = globalCache.subscribe(key, (newData) => {
data.value = newData
})
// 初始化加载
if (revalidateOnMount) {
revalidate()
}
// 定时刷新
if (refreshInterval > 0) {
timer = setInterval(revalidate, refreshInterval)
}
// 窗口聚焦时重新验证
if (revalidateOnFocus) {
const onFocus = () => {
if (revalidateTimer) {
clearTimeout(revalidateTimer)
}
revalidateTimer = setTimeout(revalidate, 1000)
}
window.addEventListener('focus', onFocus)
onUnmounted(() => {
window.removeEventListener('focus', onFocus)
})
}
onUnmounted(() => {
unsubscribe()
if (timer) clearInterval(timer)
if (revalidateTimer) clearTimeout(revalidateTimer)
})
return {
data,
isLoading,
error,
revalidate,
mutate: (newData: T) => {
globalCache.set(key, newData)
data.value = newData
}
}
}
在组件中使用
html
<template>
<div>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">出错了: {{ error.message }}</div>
<div v-else>
<h2>{{ data.name }}</h2>
<p>{{ data.description }}</p>
<button @click="refresh">刷新</button>
</div>
</div>
</template>
<script setup>
import { useSWR } from './composables/useSWR'
const { data, isLoading, error, revalidate } = useSWR(
'product-detail',
() => api.getProduct(123),
{
revalidateOnFocus: true,
refreshInterval: 30000, // 30秒刷新一次
dedupingInterval: 2000 // 2秒内重复请求去重
}
)
function refresh() {
revalidate()
}
</script>
缓存策略的选择矩阵
缓存策略决策树
各种缓存的适用场景
| 缓存类型 | 适用场景 | 容量 | 持久化 | 速度 | 复杂度 |
|---|---|---|---|---|---|
| HTTP 缓存 | 静态资源、API 响应 | 不限 | 是 | 快 | 低 |
| Service Worker | 离线应用、PWA | 不限 | 是 | 快 | 中 |
| 内存缓存 | 频繁访问的小数据 | 几十MB | 否 | 极快 | 低 |
| localStorage | 用户设置、小配置 | 5-10MB | 是 | 中 | 低 |
| IndexedDB | 大量结构化数据 | 数百MB | 是 | 中 | 高 |
| SWR | API 数据 | 可控 | 可选 | 快 | 中 |
缓存设计的黄金法则
-
分层缓存:结合多种缓存,发挥各自优势
内存缓存 → 持久化缓存 → 网络请求 -
设置过期时间:避免数据永久陈旧
-
实现失效策略:数据更新时及时清除缓存
-
控制缓存大小:避免内存溢出
-
监控缓存命中率:优化缓存策略
-
提供降级方案:缓存失效时能优雅降级
最终建议
缓存不是越多越好,而是要在数据新鲜度 和访问速度之间找到平衡:
- 几乎不变的数据(国家列表、静态配置):永久缓存
- 偶尔变化的数据(用户信息、商品详情):缓存几分钟到几小时
- 实时数据(股票价格、在线状态):不缓存或短时间缓存
结语
没有最好的缓存策略,只有最适合业务场景的缓存策略,根据数据的特性选择合适的策略,才能让我们的应用真正"快如闪电"。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!