说实话,我刚入行的时候,总觉得“能跑就行”。直到有一天,我们的核心业务页在 4G 网络下白屏了整整 8 秒,用户投诉像雪片一样飞来,老板把我叫到办公室,指着屏幕上那张长得离谱的水域瀑布图说:“这是你写的?”
那一刻我才明白,前端性能不是“锦上添花”,而是“生死线”。今天我不跟你讲什么大道理,就聊聊我踩过的那些坑,以及这 5 个真正能救命的具体代码技巧。
一、先别急着优化,你得看见问题
在我讲代码之前,先让你看看一个典型的“性能灾难现场”。
假设你在做一个电商首页,需要加载用户信息、商品列表、推荐算法、促销 banner 等等。很多新手(包括曾经的我)会这么写:
// 糟糕的写法:每个接口单独请求,完全并发,没有优化
async function loadHomepage() {
const userInfo = await fetch('/api/user/profile').then(r => r.json());
const products = await fetch('/api/products/list').then(r => r.json());
const recommendations = await fetch('/api/products/recommend').then(r => r.json());
const banners = await fetch('/api/banners').then(r => r.json());
renderPage(userInfo, products, recommendations, banners);
}
看起来没问题?太天真了。你打开 Chrome 的 Network 面板,你会发现:
- 发了 4 个请求
- 每个请求都带有认证 token(重复传输)
- 用户信息只有 2KB,但请求头占了 5KB
- 总等待时间是其中最慢的那个接口
- 如果其中任何一个接口挂了,整个页面白屏
这就是典型的“请求洪水”。接下来,我用 5 个具体的代码技巧,帮你把这些洪水变成溪流。
二、技巧一:请求去重——别让同一个接口被请求两次
问题场景
想象一下,你的首页组件是一个大杂烩:
- 顶部导航栏组件需要用户信息
- 侧边栏组件也需要用户信息
- 个人中心弹窗也需要用户信息
三个组件,三个 fetch('/api/user/profile')。结果呢?网络请求重复了 3 次,带宽浪费了 3 倍,服务器压力翻了 3 倍。
解决方案:请求缓存池
我用一个简单但有效的方案——请求缓存池。核心思想是:如果某个请求正在执行中,就返回同一个 Promise,而不是重新发起请求。
// 请求缓存池,防止重复请求
const pendingRequests = new Map();
function cachedFetch(url, options = {}) {
// 生成唯一的请求 key(考虑 URL 和请求体)
const key = JSON.stringify({ url, options });
// 如果这个请求正在执行中,返回已有的 Promise
if (pendingRequests.has(key)) {
console.log(`[请求去重] 复用已有请求: ${url}`);
return pendingRequests.get(key);
}
// 发起新请求
const promise = fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.finally(() => {
// 请求完成(成功或失败)后,从缓存中移除
pendingRequests.delete(key);
});
// 将 Promise 存入缓存
pendingRequests.set(key, promise);
return promise;
}
// 使用示例
async function loadHomepage() {
// 三个地方都需要用户信息,但只会发一次请求
const userInfo = cachedFetch('/api/user/profile');
const products = await cachedFetch('/api/products/list');
const recommendations = await cachedFetch('/api/products/recommend');
const banners = await cachedFetch('/api/banners');
// 等待所有请求完成
const [user] = await Promise.all([userInfo]);
renderPage(user, products, recommendations, banners);
}
为什么这个技巧有效?
- 减少网络请求次数:重复请求变成一次,带宽直接节省 66%
- 保证数据一致性:所有组件使用的是同一个 Promise,数据完全一致
- 自动清理:
.finally()确保请求完成后自动清理缓存,不会内存泄漏 - 零侵入性:你只需要替换
fetch为cachedFetch,其他代码不用改
进阶:带过期时间的缓存
有时候,你希望缓存只在短时间内有效。比如用户信息每 5 分钟更新一次:
const requestCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
function smartCachedFetch(url, options = {}) {
const key = JSON.stringify({ url, options });
const now = Date.now();
// 检查缓存是否过期
if (requestCache.has(key)) {
const { promise, timestamp } = requestCache.get(key);
if (now - timestamp < CACHE_TTL) {
console.log(`[智能缓存] 命中缓存: ${url} (${Math.floor((now - timestamp) / 1000)}s 前)`);
return promise;
}
// 缓存过期,清理旧缓存
requestCache.delete(key);
}
const promise = fetch(url, options)
.then(res => res.json())
.finally(() => {
requestCache.delete(key);
});
// 存入缓存,记录时间戳
requestCache.set(key, { promise, timestamp: now });
return promise;
}
这个版本更实用,因为你可以根据业务需求灵活控制缓存时间。
三、技巧二:请求合并——把多个小请求打包成大请求
问题场景
假设你的页面需要加载 10 个商品详情,每个商品需要单独请求:
// 糟糕的写法:10 个独立请求
const productIds = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
for (const id of productIds) {
const product = await fetch(`/api/products/${id}`).then(r => r.json());
renderProduct(product);
}
10 个请求,串行执行,总耗时 = 10 × 单个请求耗时。如果每个请求需要 200ms,总耗时就是 2 秒!而且每个请求都有 HTTP 开销(TCP 握手、TLS 协商、HTTP 头传输)。
解决方案:批量请求接口
大多数后端都支持批量查询。比如:
// 好的写法:一次请求获取所有数据
const productIds = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
const products = await fetch('/api/products/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: productIds }),
}).then(r => r.json());
// products 是一个数组,直接渲染
products.forEach(renderProduct);
从 10 个请求变成 1 个请求,性能提升 10 倍不止。
如果你没有批量接口怎么办?
有些老旧系统没有批量接口,你也可以在前端模拟批量请求:
// 前端模拟批量请求(适用于没有批量接口的情况)
async function batchFetch(urls, batchSize = 5) {
const results = [];
// 分批请求,避免一次性发出太多请求
for (let i = 0; i < urls.length; i += batchSize) {
const batch = urls.slice(i, i + batchSize);
// 并发执行一批请求
const batchResults = await Promise.all(
batch.map(url => fetch(url).then(r => r.json()))
);
results.push(...batchResults);
// 可选:批次之间加一点延迟,避免压垮服务器
await new Promise(resolve => setTimeout(resolve, 100));
}
return results;
}
// 使用示例
const productIds = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
const urls = productIds.map(id => `/api/products/${id}`);
const products = await batchFetch(urls);
这个方案虽然不是真正的批量接口,但至少控制了并发数量,避免了“请求洪水”。
更高级的合并:跨组件请求合并
有时候,不同组件需要不同的数据,但这些数据来自同一个接口。比如:
- 导航栏需要用户信息
- 侧边栏需要用户最近浏览记录
- 主内容区需要用户订单列表
这些数据都来自 /api/user/data,但前端拆成了三个请求。你可以让后端提供一个统一接口:
// 前端:合并请求
async function loadUserData() {
// 一次请求获取所有用户相关数据
const userData = await fetch('/api/user/complete-data').then(r => r.json());
return {
profile: userData.profile,
recentViews: userData.recentViews,
orders: userData.orders,
};
}
// 后端(Node.js 示例)
app.get('/api/user/complete-data', async (req, res) => {
const userId = req.user.id;
// 并发查询三个数据源
const [profile, recentViews, orders] = await Promise.all([
db.users.findById(userId),
db.recentViews.findByUserId(userId),
db.orders.findByUserId(userId),
]);
res.json({ profile, recentViews, orders });
});
这样,前端只需要一个请求,后端用 Promise.all 并发查询,整体性能大幅提升。
四、技巧三:请求优先级调度——别让所有请求同时抢资源
问题场景
你的首页需要加载:
- 用户信息(关键,必须优先加载)
- 商品列表(重要,但可以先显示骨架屏)
- 推荐算法(不重要,可以延迟加载)
- 促销 Banner(不重要,可以延迟加载)
如果你让所有请求同时发起,浏览器会同时处理 4 个请求,每个请求的响应时间都会变长。而且,用户可能已经看到了页面,但还在等待不重要的数据。
解决方案:请求优先级队列
我用一个简单但有效的优先级调度器:
class RequestScheduler {
constructor() {
this.queue = new Map(); // URL -> { promise, priority, status }
this.priorityOrder = ['high', 'medium', 'low'];
}
// 发起请求,带优先级
fetchWithPriority(url, options = {}, priority = 'medium') {
const key = url;
// 如果请求已经在队列中,返回已有 Promise
if (this.queue.has(key)) {
return this.queue.get(key).promise;
}
// 创建请求
const promise = fetch(url, options)
.then(res => res.json())
.finally(() => {
this.queue.delete(key);
});
// 加入队列
this.queue.set(key, { promise, priority, status: 'pending' });
// 根据优先级调度
this.schedule(priority);
return promise;
}
// 按优先级调度请求
schedule(priority) {
// 获取当前优先级的请求
const pendingRequests = Array.from(this.queue.values())
.filter(item => item.priority === priority && item.status === 'pending');
// 限制并发数(高优先级最多 2 个,中等 3 个,低等 1 个)
const maxConcurrent = { high: 2, medium: 3, low: 1 };
const limit = maxConcurrent[priority] || 1;
// 执行请求
pendingRequests.slice(0, limit).forEach(item => {
item.status = 'running';
item.promise.finally(() => {
item.status = 'done';
});
});
}
// 等待所有请求完成
async waitForAll() {
const allPromises = Array.from(this.queue.values())
.map(item => item.promise);
return Promise.all(allPromises);
}
}
// 使用示例
const scheduler = new RequestScheduler();
// 高优先级:用户信息
const userInfo = scheduler.fetchWithPriority(
'/api/user/profile',
{},
'high'
);
// 中优先级:商品列表
const products = scheduler.fetchWithPriority(
'/api/products/list',
{},
'medium'
);
// 低优先级:推荐算法
const recommendations = scheduler.fetchWithPriority(
'/api/products/recommend',
{},
'low'
);
// 低优先级:促销 Banner
const banners = scheduler.fetchWithPriority(
'/api/banners',
{},
'low'
);
// 先渲染关键内容
const [user] = await Promise.all([userInfo]);
renderHeader(user);
// 等待中优先级
const [products] = await Promise.all([products]);
renderProductList(products);
// 等待低优先级(可以延迟渲染)
setTimeout(async () => {
const [recommendations, banners] = await Promise.all([
recommendations,
banners,
]);
renderRecommendations(recommendations);
renderBanners(banners);
}, 500); // 延迟 500ms 再渲染低优先级内容
为什么这个方案有效?
- 关键内容优先:用户信息先加载,页面可以快速显示核心内容
- 控制并发:低优先级请求不会抢占高优先级请求的资源
- 延迟加载:不重要的内容可以延迟渲染,减少首屏等待时间
- 灵活可调:你可以随时调整优先级和并发限制
更实用的方案:React Suspense + 优先级
如果你用 React,可以用更优雅的方式:
import { Suspense, lazy } from 'react';
// 懒加载组件
const UserInfo = lazy(() => import('./UserInfo'));
const ProductList = lazy(() => import('./ProductList'));
const Recommendations = lazy(() => import('./Recommendations'));
function HomePage() {
return (
<div>
{/* 高优先级:立即加载 */}
<Suspense fallback={<LoadingSpinner />}>
<UserInfo />
</Suspense>
{/* 中优先级:稍后加载 */}
<Suspense fallback={<Skeleton />}>
<ProductList />
</Suspense>
{/* 低优先级:延迟加载 */}
<Suspense fallback={null}>
<Recommendations />
</Suspense>
</div>
);
}
配合 useDeferredValue 或 useTransition,你可以更精细地控制渲染优先级。
五、技巧四:请求缓存与 stale-while-revalidate——让用户感觉很快
问题场景
用户刷新页面后,所有数据都需要重新加载。即使数据几秒前刚加载过,用户还是要等。这种感觉非常差。
解决方案:内存缓存 + 后台更新
我用一个“ stale-while-revalidate ”策略:先显示缓存数据(快速),同时后台重新获取最新数据(更新缓存)。
”`javascript class CacheManager { constructor() {
this.cache = new Map();
this.ttl = 60 * 1000; // 默认 1 分钟 TTL
}
// 获取数据,优先使用缓存 async get(url, fetcher, ttl = this.ttl) {
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log(`[缓存命中] ${url} (${Math.floor((Date.now() - cached.timestamp) / 1000)}s 前)`);
// 后台重新获取,更新缓存
this.refresh(url, fetcher, ttl).catch(console.error);
return cached.data;
}
// 缓存未命中,发起请求
console.log(`[缓存未命中] ${url}`);
const data = await fetcher();
this.cache.set(url, { data, timestamp: Date.now(), ttl });
return data;
}
// 后台刷新缓存 async refresh(url, fetcher, ttl) {
try {
const data = await fetcher();
this.cache.set(url, { data, timestamp: Date.now(), ttl });
console.log(`[缓存更新] ${url}`);
} catch (error) {
console.error(`[缓存更新失败] ${url}:`, error);
}
}
// 清除指定缓存 invalidate(url) {
this.cache.delete(url);
console.log(`[缓存失效] ${url}`);
}
// 清除所有缓存 clear() {
this.cache.clear();
console.log('[缓存清除]');
} }
// 使用示例 const cache = new CacheManager();
async function loadHomepage() { // 定义数据获取函数 const fetchUserInfo = () => fetch(‘/api/user/profile’).then(r => r.json()); const fetchProducts = () => fetch(‘/api/products/list’).then(r => r.json()); const fetchRecommendations = () => fetch(‘/api/products/recommend’).then(r => r.json());
// 获取数据(优先使用缓存) const userInfo = await cache.get(‘/api/user
