今天看到一篇不错的文章,记录下来,讲的是如何解决页面卡顿问题,提高页面的性能优化,从3个场景分析,并使用代码分析
场景 1:列表渲染卡顿 ------ 从 "一次性渲染" 到 "虚拟列表"
使用vue的v-for加载10000+条数据,会明显感到页面的卡顿,
优化效果:
页面加载时间从 5.2 秒降至 0.3 秒
DOM 节点数量从 12000 + 降至 80+
滚动时无卡顿,丝滑度提升明显
bash
<div v-for="item in data"></div>
解决办法:虚拟列表
使用虚拟列表,原理是只加载可视化区域的数据,使用组件 vue-virtual-scroller,优化后的代码如下:
<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>
场景2:接口超时:从串行 到 并行加缓存
一个页面需要4个接口,使用串行方式请求,总耗时 = 接口 1 耗时 + 接口 2 耗时 + 接口 3 耗时 + 接口 4 耗时,会触发接口超时
bash
原代码:
// 错误示例:串行请求,耗时叠加
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秒(超时)
优化方案:并行请求 + 缓存复用
1、并行请求:4 个接口无依赖关系(不需要先拿到用户信息再请求订单),用Promise.all同时发起请求,总耗时 = 最长单个接口耗时
2、缓存复用:用户基本信息、订单统计等数据短时间内不会变化,用localStorage或Vuex缓存,30 分钟内重复进入页面不重复请求
bash
// 优化后:并行请求+缓存
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
场景 3:循环计算耗时 ------ 从 "原生循环" 到 "Web Worker"
一个数据可视化工具,需要对 10 万条用户行为数据做统计分析(计算留存率、转化率等),原代码在主线程中循环计算,导致页面卡顿 20 秒以上,期间无法点击任何按钮。
bash
原代码:
// 错误示例:主线程中处理大量计算
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 线程)保持空闲,用户可以正常操作页面,计算完成后通过事件通知主线程。
1、页面卡顿消失,计算期间可正常点击、滚动
2、计算总耗时略有增加(约 22 秒,因线程通信有少量开销),但用户体验大幅提升
3、若使用SharedWorker,还可实现多标签页共享计算结果(适合多窗口场景)
bash
// 创建 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();
};
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();
性能优化的 3 个核心原则
定位瓶颈再优化:用 Chrome DevTools(Performance 面板)、Vue DevTools(性能面板)等工具找到卡顿 / 超时的具体原因,不要凭感觉优化(比如盲目用for循环替代forEach,实际性能提升微乎其微)。
优先解决核心问题:先优化影响用户体验的关键场景(如页面加载、核心功能操作),再处理边缘场景
平衡优化成本与收益:不要为了 0.1 秒的性能提升,写出难以维护的代码(比如过度使用奇技淫巧的语法),可读性和可维护性同样重要。
附上参考文章:https://blog.csdn.net/panjiapengfly/article/details/151142846