在 Vue 应用开发中,性能优化是保障用户体验的关键。本文从响应式系统控制、虚拟 DOM 渲染、生命周期管理及业务逻辑优化四大维度,结合实际代码示例,深入解析 Vue3 应用性能优化的核心策略,助您构建高效稳定的前端应用。
一. 响应式系统精准控制
1.1 数据初始化优化
xml
<template>
<div>
<button @click="updateBigList">Add Item</button>
<ul>
<li v-for="item in bigList" :key="item.id">{{ item.id }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
// 初始化冻结的大数据列表,可以让 Vue 跳过响应式监听,在之前一篇文章有讲过
bigList: Object.freeze(new Array(1000).fill().map((_, i) => ({ id: i }))),
};
},
methods: {
updateBigList() {
// 模拟新元素
const newItem = { id: this.bigList.length };
// 通过新引用触发更新
this.bigList = Object.freeze([...this.bigList, newItem]);
},
},
};
</script>
在 Vue 中,要触发视图的更新,需要改变数据的引用。通过扩展运算符 ...
创建一个新的数组,将原数组的元素复制到新数组中,并添加新元素,最后将新数组赋值给 this.bigList
,这样就改变了 bigList
的引用,Vue 能够检测到这个变化并触发视图的更新。同时,每次更新后都使用 Object.freeze()
冻结新数组,确保新的数据仍然是不可变的,继续享受冻结数据带来的性能优势。
不过这样存在弊端,由于数据被冻结,无法直接修改原数组的元素。在更新数据时,必须创建新的数组,这在处理复杂的数据更新逻辑时可能会增加代码的复杂度。这种优化方案在初始化和简单的数据更新场景下能够显著提升性能,但在处理大规模数据更新和复杂业务逻辑时,就不太适合这种方式。
1.2 计算属性最佳实践
javascript
// 正确使用computed属性
export default {
data: () => ({
firstName: '王',
lastName: '小明',
products: [] // 假如他是后端返回的数据
}),
computed: {
// 缓存计算结果
fullName() {
return `${this.firstName} ${this.lastName}`
},
// 复杂数据处理
filteredProducts() {
const MAX_PRICE = 1000
return this.products
.filter(p => p.price < MAX_PRICE)
.sort((a,b) => a.price - b.price)
// 模拟一些计算操作
}
}
}
- 由于计算属性会被缓存,只要
firstName
和lastName
不发生变化,每次访问fullName
时都会直接返回之前计算好的结果,而不会重新执行计算逻辑,从而提高了性能。 - 同样,由于计算属性的缓存机制,只要
products
不发生变化,每次访问filteredProducts
时都会直接返回之前计算好的结果,避免了重复的过滤和排序操作,提高了性能。
二. 虚拟DOM渲染优化
2.1 列表渲染优化
xml
<template>
<div id="app">
<!-- 低效写法 -->
<!-- <div v-for="item in list" :key="item.id">
{{ heavyFormat(item) }}
</div> -->
<!-- 每次渲染都计算:在模板中使用 heavyFormat 方法对每个列表项进行格式化。当列表项数量较多时,每次渲染都会调用 heavyFormat 方法,这会带来大量重复的计算开销。
例如,如果列表有 1000 个项,那么 heavyFormat 方法就会被调用 1000 次。
缺乏缓存机制:这种写法没有对计算结果进行缓存,即使相同的数据多次渲染,也会重复执行计算逻辑,浪费了大量的 CPU 资源。 -->
<!-- 优化方案 -->
<div v-for="item in processedList" :key="item.raw.id">
<p>{{ item.compressed.displayName }}</p>
<p>{{ item.compressed.price }}</p>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
list: [
{ id: 1, firstName: "张", lastName: "三", price: 19.99 },
{ id: 2, firstName: "李", lastName: "四", price: 29.99 },
{ id: 3, firstName: "王", lastName: "五", price: 39.99 },
],
};
},
computed: {
processedList() {
return this.list.map((item) => ({
compressed: this.compressData(item),
raw: Object.freeze(item),
}));
},
},
methods: {
heavyFormat(item) {
return `${item.firstName} ${item.lastName} - ¥${item.price.toFixed(2)}`;
},
compressData(item) {
// 预处理减少模板计算量
return {
displayName: `${item.firstName} ${item.lastName}`,
price: `¥${item.price.toFixed(2)}`,
};
},
},
};
</script>
- 提前计算 :使用计算属性
processedList
对列表数据进行预处理。在processedList
中,调用compressData
方法对每个列表项进行格式化处理,将处理结果存储在compressed
属性中。这样在模板渲染时,直接使用预处理好的数据,避免了在模板中频繁调用计算方法。 - 缓存机制 :计算属性具有缓存特性,只有当依赖的数据(这里是
list
)发生变化时,才会重新计算processedList
。如果list
没有变化,多次访问processedList
都会直接返回之前计算好的结果,减少了不必要的计算开销。
三. 生命周期优化
3.1 内存泄漏防范
xml
<template>
<div>
<p>当前计数: {{ count }}</p>
</div>
</template>
<script>
export default {
data() {
return {
timers: [],
count: 0,
};
},
methods: {
refreshData() {
// 每次定时器触发时,将计数加 1
this.count++;
console.log("数据已刷新,当前计数:", this.count);
// 你可以在这里添加更复杂的数据刷新逻辑
// 例如从服务器获取新数据
// EventBus.$emit('update', newData);
},
},
mounted() {
// 启动定时器,每 5 秒调用一次 refreshData 方法
this.timers.push(setInterval(this.refreshData, 5000));
},
beforeDestroy() {
// 清理所有定时器
this.timers.forEach(clearInterval);
console.log("组件销毁,定时器已清理");
},
};
</script>
beforeDestroy
钩子在组件销毁前清理所有定时器,避免内存泄漏。
3.2 局部响应式数据
在某些情况下,并非所有数据都需要响应式处理。对于一些不会在模板中绑定或不会触发视图更新的数据,可以直接赋值 this 来存储,而不是放在 data
选项中。
javascript
export default {
mounted() {
this.timer = setInterval(() => console.log(123), 1000);
},
destroyed() {
clearTimeout(this.timer);
},
};
通常用在一些事件监听上面,因为事件监听并不需要响应式
四. 业务优化
4.1 防抖、节流
在处理一些高频触发的事件(如滚动、输入框输入等)时,使用节流和防抖可以减少不必要的函数调用,提高性能。
xml
<template>
<input v-model="inputValue" @input="handleInput" />
</template>
<script>
import { debounce } from 'lodash';
export default {
data() {
return {
inputValue: ''
};
},
methods: {
handleInput: debounce(function () {
// 处理输入事件
console.log('Input value:', this.inputValue);
}, 300)
}
};
</script>
4.2 异步组件加载
对于一些不影响首屏加载的组件,可以使用异步组件来实现懒加载,减少首屏加载时间。
xml
<template>
<div>
<button @click="showAsyncComponent = true">显示异步组件</button>
<!-- 初始时不会加载其代码。当用户点击按钮,showAsyncComponent 变为 true 时,Vue 会动态加载 AsyncComponent.vue 并渲染该组件。 -->
<AsyncComponent v-if="showAsyncComponent" />
</div>
</template>
<script>
const AsyncComponent = () => import("./AsyncComponent.vue");
export default {
components: {
AsyncComponent,
},
data() {
return {
showAsyncComponent: false,
};
},
};
</script>
const AsyncComponent = () => import('./AsyncComponent.vue');
采用了 ES6 的动态导入(import()
)语法,这是一种异步加载模块的方式。当使用这个异步组件时,Vue 不会在初始加载时就把 AsyncComponent.vue
对应的 JavaScript 代码打包到主文件中,而是在需要渲染该组件时,才会动态地去加载这个组件的代码。