作为程序员,我们每天都在和代码打交道 —— 实现功能、修复 BUG、迭代需求,但往往容易忽略一个关键问题:代码能跑通,不代表跑得好。当用户反馈 “页面加载要等 3 秒”“数据导出时浏览器直接卡死”“接口高峰期频繁超时”,这些看似偶然的问题,背后往往藏着性能优化的漏洞。
今天结合 3 个真实项目场景,带你拆解代码优化的思路、工具和避坑点,看完就能用到实际开发中。
一、场景 1:列表渲染卡顿 —— 从 “一次性渲染” 到 “虚拟列表”
问题背景
前段时间接手一个管理系统,用户需要查看上万条订单数据,原代码直接用v-for(Vue 项目)循环渲染所有数据,打开页面时浏览器直接卡顿 5 秒以上,甚至出现 “未响应” 提示。
<template>
<!-- 错误示例:一次性渲染10000+条数据 -->
<div class="order-item" v-for="item in allOrders" :key="item.id">
{{ item.orderNo }} - {{ item.amount }}
</div>
</template>
<script>
export default {
data() {
return {
allOrders: [] // 存储10000+条订单数据
}
},
mounted() {
this.getAllOrders(); // 一次性请求所有数据
},
methods: {
getAllOrders() {
// 直接请求全量数据,未做分页或懒加载
api.get('/orders/all').then(res => {
this.allOrders = res.data;
});
}
}
}
</script>
问题根源:DOM 节点数量过多(上万条数据对应上万个子节点),浏览器渲染时回流重绘成本极高,导致主线程阻塞。
优化方案:虚拟列表
核心思路:只渲染可视区域内的列表项,滚动时动态替换可视区域的内容,DOM 节点数量始终保持在几十到上百个(取决于可视区域高度)。
优化后代码(基于vue-virtual-scroller)
- 安装依赖:
npm install vue-virtual-scroller --save
- 组件中使用:
<template> <virtual-scroller class="order-list" :items="allOrders" :item-height="60" // 每个列表项的固定高度 key-field="id" > <template v-slot="{ item }"> <div class="order-item"> {{ item.orderNo }} - {{ item.amount }} </div> </template> </virtual-scroller> </template> <script> import { VirtualScroller } from 'vue-virtual-scroller'; import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'; export default { components: { VirtualScroller }, data() { return { allOrders: [] } }, mounted() { this.getAllOrders(); }, methods: { getAllOrders() { api.get('/orders/all').then(res => { this.allOrders = res.data; // 优化点:如果数据量超10万,可配合后端做“滚动加载” // (即滚动到底部时请求下一页数据,避免一次性加载过多) }); } } } </script> <style scoped> .order-list { height: 600px; /* 固定列表容器高度,确保可视区域可控 */ overflow-y: auto; } .order-item { height: 60px; line-height: 60px; border-bottom: 1px solid #eee; } </style>
优化效果
- 页面加载时间从 5.2 秒降至 0.3 秒
- DOM 节点数量从 12000 + 降至 80+
- 滚动时无卡顿,丝滑度提升明显
二、场景 2:接口超时 —— 从 “串行请求” 到 “并行 + 缓存”
问题背景
一个用户中心页面,需要加载用户基本信息、订单统计、收藏列表、消息通知 4 个接口数据,原代码用串行方式请求,总耗时 = 接口 1 耗时 + 接口 2 耗时 + 接口 3 耗时 + 接口 4 耗时,高峰期总耗时超 8 秒,触发接口超时。
原代码痛点
// 错误示例:串行请求,耗时叠加
async function loadUserPageData(userId) {
// 1. 请求用户基本信息(耗时2.5秒)
const userInfo = await api.get(`/user/${userId}/info`);
// 2. 请求订单统计(耗时3秒)
const orderStats = await api.get(`/user/${userId}/order-stats`);
// 3. 请求收藏列表(耗时2秒)
const collectList = await api.get(`/user/${userId}/collects`);
// 4. 请求消息通知(耗时2.8秒)
const notifications = await api.get(`/user/${userId}/notifications`);
return { userInfo, orderStats, collectList, notifications };
}
// 总耗时:2.5+3+2+2.8=10.3秒(超时)
优化方案:并行请求 + 缓存复用
核心思路:
- 并行请求:4 个接口无依赖关系(不需要先拿到用户信息再请求订单),用Promise.all同时发起请求,总耗时 = 最长单个接口耗时
- 缓存复用:用户基本信息、订单统计等数据短时间内不会变化,用localStorage或Vuex缓存,30 分钟内重复进入页面不重复请求
优化后代码
// 优化后:并行请求+缓存
async function loadUserPageData(userId) {
// 1. 定义缓存key和过期时间(30分钟)
const CACHE_KEY = `userPageData_${userId}`;
const CACHE_EXPIRE = 30 * 60 * 1000;
// 2. 先查缓存,未过期则直接返回
const cacheData = localStorage.getItem(CACHE_KEY);
if (cacheData) {
const { data, timestamp } = JSON.parse(cacheData);
if (Date.now() - timestamp < CACHE_EXPIRE) {
console.log('使用缓存数据');
return data;
}
}
// 3. 并行发起4个接口请求
const [userInfoRes, orderStatsRes, collectListRes, notificationsRes] = await Promise.all([
api.get(`/user/${userId}/info`),
api.get(`/user/${userId}/order-stats`),
api.get(`/user/${userId}/collects`),
api.get(`/user/${userId}/notifications`)
]);
// 4. 整理数据并缓存
const result = {
userInfo: userInfoRes.data,
orderStats: orderStatsRes.data,
collectList: collectListRes.data,
notifications: notificationsRes.data
};
localStorage.setItem(CACHE_KEY, JSON.stringify({
data: result,
timestamp: Date.now()
}));
return result;
}
// 总耗时:取最长接口耗时(3秒),且重复进入页面时耗时≈0
额外优化点
- 如果某个接口(如消息通知)非核心,可做 “延迟加载”(页面加载完成后再请求)
- 用Promise.allSettled替代Promise.all,避免一个接口失败导致所有请求失效(非核心接口可容忍失败)
三、场景 3:循环计算耗时 —— 从 “原生循环” 到 “Web Worker”
问题背景
一个数据可视化工具,需要对 10 万条用户行为数据做统计分析(计算留存率、转化率等),原代码在主线程中循环计算,导致页面卡顿 20 秒以上,期间无法点击任何按钮。
原代码痛点
// 错误示例:主线程中处理大量计算
function calculateUserBehavior(data) {
const result = {
retentionRate: 0,
conversionRate: 0,
activeUsers: []
};
// 循环10万条数据做复杂计算(耗时20+秒)
for (let i = 0; i < data.length; i++) {
const user = data[i];
// 1. 计算留存率(判断用户是否连续两天活跃)
if (user.activeDays >= 2) {
result.retentionRate += 1;
}
// 2. 计算转化率(判断用户是否完成付费)
if (user.paid) {
result.conversionRate += 1;
}
// 3. 筛选活跃用户(近7天活跃超3次)
if (user.recentActiveDays >= 3) {
result.activeUsers.push(user.id);
}
}
// 计算最终比率
result.retentionRate = (result.retentionRate / data.length * 100).toFixed(2) + '%';
result.conversionRate = (result.conversionRate / data.length * 100).toFixed(2) + '%';
return result;
}
// 调用后主线程阻塞
const behaviorData = await api.get('/user/behavior');
const result = calculateUserBehavior(behaviorData.data); // 页面卡顿20秒
优化方案:Web Worker
核心思路:把复杂计算逻辑转移到子线程(Web Worker)中处理,主线程(UI 线程)保持空闲,用户可以正常操作页面,计算完成后通过事件通知主线程。
优化后代码
- 创建 Worker 文件(behavior-calculator.worker.js)
// 子线程:处理计算逻辑,不操作DOM self.onmessage = function(e) { const data = e.data; // 接收主线程传递的数据 const result = { retentionRate: 0, conversionRate: 0, activeUsers: [] }; // 10万条数据计算(在子线程中执行,不阻塞主线程) for (let i = 0; i < data.length; i++) { const user = data[i]; if (user.activeDays >= 2) result.retentionRate += 1; if (user.paid) result.conversionRate += 1; if (user.recentActiveDays >= 3) result.activeUsers.push(user.id); } // 计算比率 result.retentionRate = (result.retentionRate / data.length * 100).toFixed(2) + '%'; result.conversionRate = (result.conversionRate / data.length * 100).toFixed(2) + '%'; // 向主线程发送计算结果 self.postMessage(result); // 关闭Worker(计算完成后释放资源) self.close(); };
- 主线程中使用 Worker
async function loadAndCalculateBehavior() { // 1. 请求数据 const behaviorData = await api.get('/user/behavior'); const rawData = behaviorData.data; // 2. 创建Worker(注意:本地开发需启动服务,直接打开HTML会跨域) const calculatorWorker = new Worker('./behavior-calculator.worker.js'); // 3. 向Worker发送数据 calculatorWorker.postMessage(rawData); // 4. 接收Worker返回的结果 calculatorWorker.onmessage = function(e) { const result = e.data; console.log('计算完成', result); // 更新页面UI(主线程操作,安全) renderBehaviorResult(result); }; // 5. 处理Worker错误 calculatorWorker.onerror = function(error) { console.error('Worker计算出错', error); calculatorWorker.close(); }; // 优化点:如果用户在计算过程中离开页面,主动关闭Worker window.addEventListener('beforeunload', () => { calculatorWorker.close(); }); } // 调用后:页面可正常操作,计算在后台执行 loadAndCalculateBehavior();
优化效果
- 页面卡顿消失,计算期间可正常点击、滚动
- 计算总耗时略有增加(约 22 秒,因线程通信有少量开销),但用户体验大幅提升
- 若使用SharedWorker,还可实现多标签页共享计算结果(适合多窗口场景)
四、性能优化的 3 个核心原则
通过以上 3 个场景,我们可以总结出代码优化的通用思路:
- 定位瓶颈再优化:用 Chrome DevTools(Performance 面板)、Vue DevTools(性能面板)等工具找到卡顿 / 超时的具体原因,不要凭感觉优化(比如盲目用for循环替代forEach,实际性能提升微乎其微)。
- 优先解决核心问题:先优化影响用户体验的关键场景(如页面加载、核心功能操作),再处理边缘场景。
- 平衡优化成本与收益:不要为了 0.1 秒的性能提升,写出难以维护的代码(比如过度使用奇技淫巧的语法),可读性和可维护性同样重要。
最后,性能优化不是一次性的工作,而是持续迭代的过程。建议大家在项目上线后,定期用监控工具(如 Sentry、阿里云 ARMS)跟踪性能指标,一旦发现异常及时优化。如果大家有其他优化场景或问题,欢迎在评论区交流~