在 Vue 应用开发过程中,通过合理使用框架特性、避免性能陷阱、优化组件设计等方式,从代码层面提升应用性能。
优化实践
使用内置 API 完成需求
JIT 是浏览器中的 JavaScript 引擎在运行时,把热点 JavaScript 代码即时编译为机器码,从而提高执行性能的一种机制。
Chrome 浏览器 JIT 指内置的 JavaScript 引擎 V8 的即时编译机制。
使用 DOM API / BOM API / Vue API 完成开发,DOM API / BOM API / Vue API 通常有对应的优化算法,必要时或节约开发成本时再使用第三方工具类库辅助。
- API 底层都是基于 V8 引擎,相比 JS 层 JIT 编译执行而言,基于 C++ 的 V8 会更快。
sort会根据情况采用归并或者插入排序(小数组采用插入,大数组采用归并或者是归并 + 插入的混合排序方案),相比手动实现的 JS 排序功能更优。- 遍历时使用
for...of/for代替for...in。for...in语义需要收集对象自身的枚举属性,然后再去遍历原型链上的属性,过滤掉不可枚举的属性值,然后处理属性遮蔽问题,最终动态决定遍历结果,这个无法预测的枚举流程是无法被跳过的,在数组遍历时只能执行慢路径查找。
javascript
// 推荐:使用 for...of 或 for 循环
const arr = [1, 2, 3, 4, 5];
for (const item of arr) {
console.log(item);
}
// 或使用传统 for 循环
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 避免:使用 for...in 遍历数组(性能较差)
for (const index in arr) {
console.log(arr[index]);
}
避免非必要响应式
避免将非响应式实例声明为响应式,尤其注意不要将插件实例声明为响应式,在 Vue2 中会由于深度递归响应,导致大量内存被占用,严重会出现假死的情况。
在 Vue3 中,可以使用 shallowRef 和 shallowReactive 来避免非必要的响应逻辑。
javascript
// ❌ 将插件实例声明为响应式
export default {
data() {
return {
// Vue2 会深度递归处理,导致性能问题
echart: null,
moment: moment,
};
},
mounted(){
this.echart = echart.init();
}
};
// ✅ 使用 Object.freeze 或直接挂载到 this
export default {
data() {
return {
// 使用 Object.freeze 冻结对象,避免响应式处理
// Vue 处理响应式之前会去判断 configurable 描述符是否可用,不可用不会进行响应式处理
axios: Object.freeze(axios),
};
},
created() {
// 或直接挂载到实例上,不放入 data
this.axios = axios;
},
};
Keepalive 缓存
使用 <keep-alive> 缓存组件,根据需求进行缓存,小型项目可以直接进行 RouterView 级别缓存,大型项目指定对大型组件的缓存,同时通过 max 指定最大缓存组件数,注意要在 activated 和 deactivated 中完成组件的唤醒和卸载处理。
html
<!-- 小型项目:RouterView 级别缓存 -->
<template>
<keep-alive :max="10">
<router-view />
</keep-alive>
</template>
<!-- 大型项目:指定组件缓存 -->
<template>
<keep-alive :include="['ComponentA', 'ComponentB']" :max="5">
<component :is="currentComponent" />
</keep-alive>
</template>
<script>
export default {
name: "ComponentA",
activated() {
// 组件被激活时执行
// 完成定时器、事件、数据请求
this.setTimers();
this.refreshData();
},
deactivated() {
// 组件被停用时执行
// 销毁定时器,事件,避免内存泄漏和逻辑冲突
this.clearTimers();
},
};
</script>
v-if 和 v-show
频繁切换避免使用 v-if,使用 v-show,由于 v-show 会导致代码不太美观,可以的话通过 <component :is="bindComponent"> 或者 v-if 结合 <keep-alive> 实现类似的逻辑。
html
<template>
<!-- ❌ v-if 会销毁和重建组件 -->
<div v-if="showComponent">组件内容</div>
<!-- ✅ v-show 只切换 display 样式 -->
<div v-show="showComponent">组件内容</div>
<!-- ✅ 使用 component 动态组件 + keep-alive 组合,保持代码美观,配合异步组件优化首次渲染体积,基于 keep-alive 实现异步组件缓存策略 -->
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
</template>
<script>
export default {
components: {
// Vue3 用 defineAsyncComponent 封装引入
ComponentA: () => import("./components/componentA"),
ComponentB: () => import("./components/componentB"),
},
data() {
return {
showComponent: true,
currentComponent: "ComponentA",
};
},
};
</script>
Computed 缓存
计算内容可以用 computed 计算属性去缓存,computed 会在依赖属性更新触发视图更新时,才会根据 dirty 标志位判断依赖属性是否更新,决定计算新值还是获取缓存值。
html
<template>
<div>
<p>总价:{{ totalPrice }}</p>
<p>折扣价:{{ discountPrice }}</p>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ price: 10, quantity: 2 },
{ price: 20, quantity: 3 },
],
discount: 0.8,
};
},
computed: {
// ✅ 依赖变化时才重新计算,否则使用缓存
totalPrice() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
// 依赖 totalPrice 计算结果,totalPrice 缓存时 discount 不会重复计算
discountPrice() {
return this.totalPrice * this.discount;
},
},
// ❌ 不推荐:使用 methods 每次都会重新计算
methods: {
getTotalPrice() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
},
},
};
</script>
内部创建和销毁
销毁监听器、定时器、实例,避免出现内存泄漏影响性能问题。
html
<script>
export default {
data() {
return {
timer: null,
resizeHandler: null,
};
},
mounted() {
// 创建定时器
this.timer = setInterval(() => {
console.log("定时任务");
}, 1000);
// 小技巧:通过 AbortController 可以同时销毁多个事件监听器
this.controller = new AbortController();
window.addEventListener("scroll", this.scrollHandler, {
signal: this.controller.signal,
});
window.addEventListener("resize", this.resizeHandler, {
signal: this.controller.signal,
});
// 三方插件初始化
this.thirdPlugin = plugin.init();
},
methods: {
resizeHandler() {
// resize 事件
},
scrollHandler() {
// scroll 事件
},
},
beforeDestroy() {
// ✅ 销毁定时器
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
// ✅ 销毁事件监听
if (this.controller) {
this.controller.abort();
this.controller = null;
}
// ✅ 销毁插件实例
if (this.thirdPlugin) {
// 先看三方插件内部是否有销毁 API 主动执行,避免插件内部引用未销毁导致泄漏
this.thirdPlugin.destroy();
this.thirdPlugin = null;
}
},
};
</script>
异步组件
Vue-Router 和 Vue 通过异步组件方式按需引入,必要时再进行加载。
但是要避免过度拆分,当 组件拆分前请求成本(请求耗时、请求体积、是否重复) > 组件拆分后的请求成本 时,再考虑进行拆分。
javascript
import Home from "@/views/Home.vue";
// Vue Router 异步组件
const routes = [
{
path: "/home",
// 首页不推荐异步逻辑,避免首页加载白屏
component: Home,
},
{
path: "/large-page",
// ✅ 使用 webpackChunkName 指定 chunk 名称,避免过渡拆包,可以把同个页面下的子路由都集中在webpackChunk中加载,也可以按需拆分
component: () =>
import(/* webpackChunkName: "large-page" */ "@/views/LargePage.vue"),
},
];
// Vue 组件异步加载
export default {
components: {
// ✅ 异步组件:按需加载
HeavyComponent: () => import("@/components/HeavyComponent.vue"),
// ✅ 带加载状态和错误处理, Vue3 使用 defineComponent API
AsyncComponent: () => ({
component: import("@/components/AsyncComponent.vue"),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200, // 延迟显示 loading
timeout: 3000, // 超时时间
}),
},
};
Vue 官方优化实践
- Vue.js 官方文档 - 性能优化:提供关于 v-once 和函数式组件的优化方案等。
- Vue2 Cookbook:关于 Vue2 的最佳实践。
总结
- 使用内置 API :优先使用 DOM API / BOM API / Vue API 完成开发,这些 API 基于 V8 引擎优化,性能优于手动实现的 JavaScript 代码。遍历时使用
for...of/for代替for...in,避免慢路径查找。 - 避免非必要响应式 :避免将插件实例等非响应式对象声明为响应式,在 Vue2 中会由于深度递归响应导致性能问题。使用
Object.freeze冻结对象或直接挂载到实例上,避免响应式处理。 - 组件缓存 :使用
<keep-alive>缓存组件,小型项目可以直接进行RouterView级别缓存,大型项目指定对大型组件的缓存,同时通过max指定最大缓存组件数。注意要在activated和deactivated中完成组件的唤醒和卸载处理。 - 条件渲染优化 :频繁切换避免使用
v-if,使用v-show。可以通过<component :is="bindComponent">或者v-if结合<keep-alive>实现类似的逻辑,保持代码美观的同时优化性能。 - 计算属性缓存 :使用
computed计算属性缓存计算结果,computed会在依赖属性更新触发视图更新时,根据dirty标志位判断是否重新计算,否则使用缓存值。 - 资源清理 :及时销毁监听器、定时器、实例,避免内存泄漏。可以使用
AbortController同时销毁多个事件监听器,对于三方插件需要主动调用销毁 API。 - 异步组件:通过异步组件方式按需引入,优化首屏加载速度。但要避免过度拆分,需要权衡组件拆分前后的请求成本(请求耗时、请求体积、是否重复)。