
前置阅读: 这一篇建立在前四篇的基础上,强烈推荐先读:
本篇涉及多并发请求控制和缓存——这两个是让应用从"可用"变成"好用"的最后一公里。
一个团队的应用仪表板需要加载4个数据源:
用户信息 → 100ms
统计数据 → 200ms
通知列表 → 150ms
系统警告 → 300ms
初级做法:
async function loadDashboard() {
const user = await fetch('/api/user'); // 100ms
const stats = await fetch('/api/stats'); // +200ms
const notify = await fetch('/api/notifications'); // +150ms
const warnings = await fetch('/api/warnings'); // +300ms
// 总耗时:750ms ❌
}
用户反馈: "仪表板加载太慢了,有时候需要等一秒。"
技术负责人查了一下代码,发现了问题。这四个请求互不依赖,却被串行执行了。
正确做法:
async function loadDashboard() {
const [user, stats, notify, warnings] = await Promise.all([
fetch('/api/user'),
fetch('/api/stats'),
fetch('/api/notifications'),
fetch('/api/warnings')
]);
// 总耗时:300ms ✅(最慢的请求决定)
}
性能提升: 2.5倍
更复杂的问题是:如果其中某个请求失败怎么办? 是整个仪表板都加载失败,还是显示部分数据?
这决定了你用Promise.all还是Promise.allSettled。
而且,仪表板开了一整天,这些数据被重复加载了100次,每次都是从服务器获取一样的数据。如果有缓存,可以立即显示。
这一篇,我们要解决的问题就是:如何并发加载多个请求,以及如何用缓存让应用快到飞起。
数据依赖关系决定了请求方式
顺序请求(Dependent):
用户ID → 用户信息 → 用户的文章列表
↓ ↓
需要userId 需要userInfo
并行请求(Independent):
用户信息
├─ 统计数据(互不依赖)
├─ 通知列表
└─ 权限信息
// ❌ 不要这样做!这两个请求有依赖关系
asyncfunction loadUserAndPosts() {
const [user, posts] = awaitPromise.all([
fetch('/api/user/123'),
fetch('/api/posts?userId=???') // userId来自user数据,但Promise.all不会等
]);
}
// ✅ 正确做法:先获取user,再获取posts
asyncfunction loadUserAndPosts() {
const user = await fetch('/api/user/123').then(r => r.json());
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
return { user, posts };
}
// ✅ 或者用嵌套的Promise.all
asyncfunction loadUserAndPosts() {
const user = await fetch('/api/user/123').then(r => r.json());
// 这些请求都依赖user,但彼此独立
const [posts, comments, likes] = awaitPromise.all([
fetch(`/api/posts?userId=${user.id}`).then(r => r.json()),
fetch(`/api/comments?userId=${user.id}`).then(r => r.json()),
fetch(`/api/likes?userId=${user.id}`).then(r => r.json()),
]);
return { user, posts, comments, likes };
}
// 假设每个请求的延迟
const latency = {
'fetchUser': 100,
'fetchPosts': 150,
'fetchComments': 120,
'fetchLikes': 80
};
// 方案1:完全串行
// user → posts → comments → likes
// 总时间 = 100 + 150 + 120 + 80 = 450ms ❌
// 方案2:user之后并行其他
// user → [posts | comments | likes]
// 总时间 = 100 + max(150, 120, 80) = 250ms ✅
// 性能提升:1.8倍
这是一个关键的选择——取决于你对失败的容错度。
// ❌ 如果任何请求失败,catch会捕捉到
asyncfunction loadDashboard() {
try {
const [user, stats, notifications, warnings] = awaitPromise.all([
api.get('/user'),
api.get('/stats'),
api.get('/notifications'),
api.get('/warnings')
]);
// 使用所有数据
return { user, stats, notifications, warnings };
} catch (error) {
// 一个失败,整个失败
// 仪表板空白,用户看不到任何数据
console.error('加载仪表板失败:', error);
throw error;
}
}
问题: 如果某个API很慢或偶尔超时,整个仪表板都加载不了。
// ✅ 等所有请求都完成(无论成功或失败),然后检查每一个
asyncfunction loadDashboard() {
const results = awaitPromise.allSettled([
api.get('/user'),
api.get('/stats'),
api.get('/notifications'),
api.get('/warnings')
]);
// results是这样的格式:
// [
// { status: 'fulfilled', value: {...} },
// { status: 'rejected', reason: Error },
// { status: 'fulfilled', value: {...} },
// { status: 'fulfilled', value: {...} }
// ]
// 分别处理成功和失败
const data = {
user: results[0].status === 'fulfilled' ? results[0].value : null,
stats: results[1].status === 'fulfilled' ? results[1].value : null,
notifications: results[2].status === 'fulfilled' ? results[2].value : null,
warnings: results[3].status === 'fulfilled' ? results[3].value : null,
};
const errors = {
user: results[0].status === 'rejected' ? results[0].reason.message : null,
stats: results[1].status === 'rejected' ? results[1].reason.message : null,
notifications: results[2].status === 'rejected' ? results[2].reason.message : null,
warnings: results[3].status === 'rejected' ? results[3].reason.message : null,
};
return { data, errors };
}
优势: 即使某个API失败,用户仍然能看到其他部分的数据。这在现代应用中非常重要。
场景 | 用Promise.all | 用Promise.allSettled |
|---|---|---|
登录(所有字段都需要) | ✅ | ❌ |
仪表板(可以显示部分数据) | ❌ | ✅ |
支付(关键业务,一个错就要全部失败) | ✅ | ❌ |
搜索页面(多个可选过滤条件) | ❌ | ✅ |
依赖关系复杂 | ❌ | ✅ |
// ❌ 问题很多
function Dashboard() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
asyncfunction load() {
try {
const [user, stats, notify, warnings] = awaitPromise.all([
api.get('/user'),
api.get('/stats'),
api.get('/notifications'),
api.get('/warnings')
]);
setData({ user, stats, notify, warnings });
} catch (err) {
setError(err.message);
}
}
load();
}, []);
if (error) return<div>出错了</div>;
if (!data) return<div>加载中...</div>;
return (
<div>
<UserProfile user={data.user} />
<Stats stats={data.stats} />
<Notifications notify={data.notify} />
<Warnings warnings={data.warnings} />
</div>
);
}
问题:
// ✅ 生产就绪的仪表板
function Dashboard() {
const [data, setData] = useState({
user: null,
stats: null,
notifications: null,
warnings: null,
});
const [errors, setErrors] = useState({
user: null,
stats: null,
notifications: null,
warnings: null,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, 10000); // 10秒总超时
asyncfunction loadDashboard() {
try {
setLoading(true);
// ✅ 使用Promise.allSettled获得部分失败容错
const results = awaitPromise.allSettled([
api.get('/user', { signal: controller.signal }),
api.get('/stats', { signal: controller.signal }),
api.get('/notifications', { signal: controller.signal }),
api.get('/warnings', { signal: controller.signal }),
]);
if (!isMounted) return;
// 处理结果
const newData = { ...data };
const newErrors = { ...errors };
const keys = ['user', 'stats', 'notifications', 'warnings'];
keys.forEach((key, index) => {
if (results[index].status === 'fulfilled') {
newData[key] = results[index].value;
newErrors[key] = null;
} else {
// 保持之前的数据(如果有的话)
newErrors[key] = results[index].reason?.message || '加载失败';
}
});
setData(newData);
setErrors(newErrors);
} catch (error) {
if (error.name === 'AbortError') {
console.log('仪表板加载超时');
} else {
console.error('未预期的错误:', error);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
loadDashboard();
return() => {
isMounted = false;
clearTimeout(timeoutId);
controller.abort();
};
}, []);
return (
<div className="dashboard">
{/* 用户部分:可能失败 */}
{errors.user ? (
<ErrorCard message={`用户信息加载失败: ${errors.user}`} />
) : data.user ? (
<UserProfile user={data.user} />
) : (
<LoadingCard />
)}
{/* 统计部分:可能失败但其他部分仍显示 */}
{errors.stats ? (
<ErrorCard message={`统计数据加载失败: ${errors.stats}`} />
) : data.stats ? (
<StatsWidget stats={data.stats} />
) : (
<LoadingCard />
)}
{/* 通知部分 */}
{errors.notifications ? (
<ErrorCard message={`通知加载失败: ${errors.notifications}`} />
) : data.notifications ? (
<NotificationsList notifications={data.notifications} />
) : (
<LoadingCard />
)}
{/* 警告部分 */}
{errors.warnings ? (
<ErrorCard message={`警告加载失败: ${errors.warnings}`} />
) : data.warnings ? (
<WarningsPanel warnings={data.warnings} />
) : (
<LoadingCard />
)}
</div>
);
}
// 初级做法:没有缓存
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 每次userId改变都要重新fetch
api.get(`/api/users/${userId}`).then(setUser);
}, [userId]);
return<div>{user?.name}</div>;
}
// 用户快速切换:
// userId=1 → fetch /api/users/1
// userId=2 → fetch /api/users/2
// userId=1 → fetch /api/users/1 again(重复了!)
代价:
3个用户ID的频繁切换(常见场景):
├─ 网络流量:3倍
├─ 服务器查询:3倍
├─ 渲染次数:3倍
└─ 用户感受:慢,且不稳定
// ❌ 问题:永不过期
const cache = newMap();
asyncfunction fetchWithCache(url) {
// 检查缓存
if (cache.has(url)) {
console.log('💾 缓存命中:', url);
return cache.get(url);
}
// 缓存未命中,请求数据
console.log('📡 发起新请求:', url);
const data = await api.get(url).then(r => r.json());
// 存储到缓存
cache.set(url, data);
return data;
}
// 问题:如果服务器的数据更新了,客户端永远不知道
// 用户看到的始终是第一次请求的数据 ❌
// ✅ 带过期时间的缓存
class CacheWithTTL {
constructor() {
this.store = newMap();
}
/**
* 存储值到缓存
* @param key - 缓存键
* @param value - 缓存值
* @param ttl - 生存时间(毫秒),默认5分钟
*/
set(key, value, ttl = 5 * 60 * 1000) {
const expiresAt = Date.now() + ttl;
this.store.set(key, {
value,
expiresAt,
createdAt: Date.now(),
});
// 设置自动清理(避免内存泄漏)
if (ttl > 0) {
setTimeout(() => {
this.delete(key);
}, ttl);
}
}
/**
* 从缓存获取值
* @returns 如果缓存有效返回值,否则返回null
*/
get(key) {
const cached = this.store.get(key);
if (!cached) {
returnnull;
}
// 检查是否过期
if (Date.now() > cached.expiresAt) {
console.log('⏰ 缓存已过期:', key);
this.delete(key);
returnnull;
}
// 计算还剩多少时间
const remainingTime = cached.expiresAt - Date.now();
console.log(`💾 缓存命中: ${key} (还有${remainingTime}ms过期)`);
return cached.value;
}
delete(key) {
this.store.delete(key);
}
clear() {
this.store.clear();
}
// 获取缓存统计
stats() {
return {
size: this.store.size,
entries: Array.from(this.store.entries()).map(([key, val]) => ({
key,
expiresIn: val.expiresAt - Date.now(),
})),
};
}
}
// 使用
const cache = new CacheWithTTL();
asyncfunction fetchUserWithCache(userId) {
const cacheKey = `user:${userId}`;
// 先查缓存
let user = cache.get(cacheKey);
if (!user) {
// 缓存未命中,请求数据
user = await api.get(`/users/${userId}`);
// 存储5分钟
cache.set(cacheKey, user, 5 * 60 * 1000);
}
return user;
}
策略1:时间失效(TTL) — 简单但可能显示过时数据
cache.set('user:123', userData, 5 * 60 * 1000); // 5分钟过期
策略2:事件失效 — 在数据变化时主动清除缓存
// ❌ 初级做法
asyncfunction updateUser(userId, updates) {
const result = await api.patch(`/users/${userId}`, updates);
// 清除该用户的缓存
cache.delete(`user:${userId}`);
return result;
}
// ❌ 问题:还有其他缓存可能也需要清除
// 比如用户列表、用户的文章列表等
// ✅ 更好的做法:清除相关的所有缓存
asyncfunction updateUser(userId, updates) {
const result = await api.patch(`/users/${userId}`, updates);
// 清除所有相关缓存
cache.delete(`user:${userId}`);
cache.delete('users:list'); // 列表可能也需要更新
cache.delete(`user:${userId}:posts`); // 如果显示了用户的文章
return result;
}
// ✅ 最好的做法:使用缓存标签系统
class SmartCacheWithTags {
constructor() {
this.cache = newMap();
this.tags = newMap(); // key -> [tags]
}
set(key, value, ttl, tags = []) {
this.cache.set(key, { value, expiresAt: Date.now() + ttl });
// 记录这个key关联的tags
this.tags.set(key, tags);
// 也记录反向关联(tag -> keys)用于快速失效
tags.forEach(tag => {
if (!this.tags.has(tag)) {
this.tags.set(tag, newSet());
}
this.tags.get(tag).add(key);
});
}
// 根据tag失效一类缓存
invalidateTag(tag) {
const keys = this.tags.get(tag);
if (keys) {
keys.forEach(key =>this.cache.delete(key));
}
}
get(key) {
returnthis.cache.get(key)?.value;
}
}
// 使用
const smartCache = new SmartCacheWithTags();
// 存储时关联tags
smartCache.set('user:123', userData, 5 * 60 * 1000, ['user', 'user:123']);
smartCache.set('users:list', listData, 5 * 60 * 1000, ['users']);
// 用户更新后,失效相关tag
asyncfunction updateUser(userId, updates) {
await api.patch(`/users/${userId}`, updates);
// 一行代码清除所有相关缓存
smartCache.invalidateTag(`user:${userId}`);
smartCache.invalidateTag('users'); // 列表也要更新
}
策略3:Stale-While-Revalidate (SWR) — 最好的平衡
/**
* SWR(Stale-While-Revalidate)模式
* 特点:
* 1. 立即返回过期的缓存数据(快速)
* 2. 同时在后台请求新数据
* 3. 新数据到达时更新UI
*/
class SWRCache {
constructor() {
this.cache = newMap();
}
asyncget(key, fetcher, ttl = 60 * 1000) {
const cached = this.cache.get(key);
const now = Date.now();
// 情况1:有缓存且还新鲜 → 直接返回
if (cached && now < cached.expiresAt) {
return cached.value;
}
// 情况2:缓存过期了 → 立即返回旧数据,后台获取新数据
if (cached && now >= cached.expiresAt) {
// 返回旧数据
const staleData = cached.value;
// 后台更新数据(异步,不等待)
this._revalidate(key, fetcher, ttl);
return staleData;
}
// 情况3:没有缓存 → 获取新数据
returnthis._revalidate(key, fetcher, ttl);
}
async _revalidate(key, fetcher, ttl) {
try {
const freshData = await fetcher();
// 更新缓存
this.cache.set(key, {
value: freshData,
expiresAt: Date.now() + ttl,
});
return freshData;
} catch (error) {
console.error('SWR更新失败:', error);
// 如果请求失败,保持原有缓存
const cached = this.cache.get(key);
return cached?.value || null;
}
}
}
// React中使用SWR
function useSWR(key, fetcher, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const swrCache = useRef(new SWRCache());
useEffect(() => {
let isMounted = true;
const load = async () => {
try {
setLoading(true);
// 获取数据(可能是缓存)
const result = await swrCache.current.get(
key,
fetcher,
options.ttl || 5 * 60 * 1000
);
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
load();
return() => {
isMounted = false;
};
}, [key, fetcher, options.ttl]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error } = useSWR(
`user:${userId}`,
() => api.get(`/users/${userId}`),
{ ttl: 5 * 60 * 1000 }
);
if (loading) return<div>加载中...</div>;
if (error) return<div>加载失败</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>此数据可能已缓存,稍后会自动更新</p>
</div>
);
}
// api/cache.js
exportconst cache = new CacheWithTTL();
exportasyncfunction fetchUser(userId) {
const key = `user:${userId}`;
const cached = cache.get(key);
if (cached) return cached;
const user = await api.get(`/users/${userId}`);
cache.set(key, user, 10 * 60 * 1000); // 10分钟
return user;
}
exportasyncfunction fetchStats() {
const key = 'stats';
const cached = cache.get(key);
if (cached) return cached;
const stats = await api.get('/stats');
cache.set(key, stats, 5 * 60 * 1000); // 5分钟
return stats;
}
// 类似的还有fetchNotifications、fetchWarnings等
// components/OptimizedDashboard.js
function OptimizedDashboard() {
const [data, setData] = useState({
user: null,
stats: null,
notifications: null,
warnings: null,
});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
asyncfunction loadDashboard() {
try {
setLoading(true);
// 并行加载所有数据(使用带缓存的fetch函数)
const results = awaitPromise.allSettled([
fetchUser(),
fetchStats(),
fetchNotifications(),
fetchWarnings(),
]);
if (!isMounted) return;
const newData = {};
const newErrors = {};
const keys = ['user', 'stats', 'notifications', 'warnings'];
const fetchers = [fetchUser, fetchStats, fetchNotifications, fetchWarnings];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
newData[keys[index]] = result.value;
} else {
newErrors[keys[index]] = result.reason?.message;
}
});
setData(newData);
setErrors(newErrors);
} finally {
if (isMounted) {
setLoading(false);
}
}
}
loadDashboard();
return() => {
isMounted = false;
};
}, []);
// 性能指标:
// 第一次加载:300ms(最慢的API)
// 第二次打开:<1ms(所有数据都是缓存)
// 用户体验:即时响应 ✅
return (
<div className="dashboard">
{/* 渲染逻辑... */}
</div>
);
}
// 生产级缓存检查清单
// [ ] 有TTL机制,防止永久过期
// [ ] 有内存限制,防止内存泄漏
// [ ] 失效策略清晰(时间/事件/标签)
// [ ] 处理了并发请求的重复问题
// [ ] 有错误恢复机制
// [ ] 支持部分数据
// [ ] 能够手动清除缓存
// [ ] 有缓存统计和监控
// [ ] 考虑了用户登出时的清除
// [ ] 考虑了敏感数据的安全性
// ❌ 不要缓存:
// - 个人信息(隐私)
// - 财务数据(安全)
// - 实时数据(金融行情)
// - 用户权限(可能改变)
// ✅ 可以缓存:
// - 配置数据(很少变化)
// - 静态内容
// - 列表数据(带TTL)
// - 用户偏好设置
场景:加载仪表板(4个API,各200ms)
无优化:
├─ 串行请求:800ms
├─ 无缓存:每次都是800ms
└─ 用户感受:慢
并发优化:
├─ 并行请求:200ms
├─ 无缓存:仍然200ms每次
└─ 用户感受:快多了
并发+缓存:
├─ 第一次:200ms
├─ 后续:<1ms
├─ 用户感受:即时响应 ✅
性能提升:800ms → <1ms = 800倍
这一篇讲的是应用优化的最后一公里——并发控制和缓存策略。如果理解了这一篇,关注微信公众号《前端达人》,我们会继续讨论:
📚 本系列的最后内容
并发和缓存是前端性能优化的两大支柱。
掌握了这一篇的内容,你就能:
下一篇,我们会讨论React Query——为什么用它能让你少写100行缓存代码。
点赞、分享、评论支持,我们下一篇再见!