从卡顿到丝滑:3 个实战场景教你搞定代码性能优化
作为程序员,我们每天都在和代码打交道 —— 实现功能、修复 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-scrollerclass="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();};// 优化点:如果用户在计算过程中离开页面,主动关闭Workerwindow.addEventListener('beforeunload', () => {calculatorWorker.close();}); }// 调用后:页面可正常操作,计算在后台执行 loadAndCalculateBehavior();
优化效果
- 页面卡顿消失,计算期间可正常点击、滚动
- 计算总耗时略有增加(约 22 秒,因线程通信有少量开销),但用户体验大幅提升
- 若使用SharedWorker,还可实现多标签页共享计算结果(适合多窗口场景)
四、性能优化的 3 个核心原则
通过以上 3 个场景,我们可以总结出代码优化的通用思路:
- 定位瓶颈再优化:用 Chrome DevTools(Performance 面板)、Vue DevTools(性能面板)等工具找到卡顿 / 超时的具体原因,不要凭感觉优化(比如盲目用for循环替代forEach,实际性能提升微乎其微)。
- 优先解决核心问题:先优化影响用户体验的关键场景(如页面加载、核心功能操作),再处理边缘场景。
- 平衡优化成本与收益:不要为了 0.1 秒的性能提升,写出难以维护的代码(比如过度使用奇技淫巧的语法),可读性和可维护性同样重要。
最后,性能优化不是一次性的工作,而是持续迭代的过程。建议大家在项目上线后,定期用监控工具(如 Sentry、阿里云 ARMS)跟踪性能指标,一旦发现异常及时优化。如果大家有其他优化场景或问题,欢迎在评论区交流~