Vue 3 性能优化专题
概述
Vue 3 在性能方面有了显著提升,但在大型应用中仍需要合理优化。本文从编译优化、运行时优化、组件优化等多个维度,深入探讨 Vue 3 性能优化的最佳实践。
Vue 3 内置优化
编译时优化
javascript
// Vue 3 编译器的静态提升
// 模板
<template>
<div>
<span>静态内容</span>
<span>{{ dynamic }}</span>
</div>
</template>
// 编译后
const _hoisted_1 = /*#__PURE__*/ createVNode("span", null, "静态内容")
function render(_ctx) {
return createVNode("div", null, [
_hoisted_1, // 静态节点被提升
createVNode("span", null, _ctx.dynamic)
])
}PatchFlags 优化
javascript
// Vue 3 使用 PatchFlags 标记动态节点
// 只有标记的部分才会进行 diff
// 1 - TEXT:动态文本
// 2 - CLASS:动态 class
// 4 - STYLE:动态 style
// 8 - PROPS:动态 props
// 16 - FULL_PROPS:有动态 key
// 32 - HYDRATE_EVENTS:有事件监听
// 64 - STABLE_FRAGMENT:稳定的 fragment
// 128 - KEYED_FRAGMENT:有 key 的 fragment
// 256 - UNKEYED_FRAGMENT:无 key 的 fragment
// 512 - NEED_PATCH:需要 patch
// 1024 - DYNAMIC_SLOTS:动态插槽事件缓存
javascript
// Vue 3 自动缓存事件处理函数
<template>
<button @click="handleClick">Click</button>
</template>
// 编译后
function render(_ctx) {
return createVNode("button", {
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.handleClick($event))
}, "Click")
}响应式优化
shallowRef 和 shallowReactive
javascript
import { shallowRef, shallowReactive, triggerRef } from 'vue';
// shallowRef - 只有 .value 的变化是响应式的
const state = shallowRef({ count: 0 });
state.value.count++; // 不触发更新
state.value = { count: 1 }; // 触发更新
// 手动触发更新
state.value.count++;
triggerRef(state); // 强制触发更新
// shallowReactive - 只有第一层是响应式的
const obj = shallowReactive({
nested: { count: 0 }
});
obj.nested.count++; // 不触发更新
obj.nested = { count: 1 }; // 触发更新markRaw - 跳过响应式
javascript
import { markRaw, reactive } from 'vue';
// 大型对象或第三方库实例
const rawData = markRaw({
largeArray: new Array(10000).fill(0),
thirdPartyLib: new SomeLibrary()
});
const state = reactive({
data: rawData // 不会被转为响应式
});toRaw - 获取原始对象
javascript
import { reactive, toRaw } from 'vue';
const original = { count: 0 };
const proxy = reactive(original);
// 需要传递原始对象给第三方库时
const raw = toRaw(proxy);
someThirdPartyLib.process(raw);避免不必要的响应式
javascript
// 不好:所有数据都是响应式
const state = reactive({
users: [], // 大数组
config: { /* 静态配置 */ }
});
// 好:只对需要响应的数据使用响应式
const users = shallowRef([]);
const config = { /* 静态配置 */ }; // 普通对象计算属性优化
合理使用 computed
javascript
import { computed, ref } from 'vue';
const items = ref([/* 大量数据 */]);
const filter = ref('');
// 好:使用 computed 缓存计算结果
const filteredItems = computed(() => {
return items.value.filter(item =>
item.name.includes(filter.value)
);
});
// 不好:每次渲染都重新计算
// const filteredItems = items.value.filter(...)computed 的 getter/setter
javascript
const firstName = ref('John');
const lastName = ref('Doe');
// 带 setter 的 computed
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
}
});避免 computed 中的副作用
javascript
// 不好:computed 中有副作用
const badComputed = computed(() => {
console.log('computed'); // 副作用
someApiCall(); // 副作用
return data.value * 2;
});
// 好:computed 保持纯净
const goodComputed = computed(() => data.value * 2);
// 副作用使用 watchEffect
watchEffect(() => {
console.log('effect');
someApiCall();
});组件优化
v-once - 只渲染一次
vue
<template>
<!-- 静态内容只渲染一次 -->
<div v-once>
<h1>{{ title }}</h1>
<p>{{ staticContent }}</p>
</div>
</template>v-memo - 条件缓存
vue
<template>
<!-- 只有当 item.id 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
<ExpensiveComponent :data="item" />
</div>
<!-- 条件缓存 -->
<div v-memo="[selected === item.id]">
<p>{{ item.name }}</p>
<p>Selected: {{ selected === item.id }}</p>
</div>
</template>异步组件
javascript
import { defineAsyncComponent } from 'vue';
// 基本用法
const AsyncComp = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
);
// 带配置
const AsyncCompWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // 显示 loading 前的延迟
timeout: 10000, // 超时时间
suspensible: false // 是否支持 Suspense
});KeepAlive 缓存组件
vue
<template>
<!-- 缓存组件状态 -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
<!-- 指定缓存组件 -->
<KeepAlive include="Home,About">
<component :is="currentTab" />
</KeepAlive>
<!-- 排除组件 -->
<KeepAlive exclude="Settings">
<component :is="currentTab" />
</KeepAlive>
<!-- 限制缓存数量 -->
<KeepAlive :max="10">
<component :is="currentTab" />
</KeepAlive>
</template>
<script setup>
import { onActivated, onDeactivated } from 'vue';
// KeepAlive 生命周期
onActivated(() => {
console.log('组件被激活');
});
onDeactivated(() => {
console.log('组件被缓存');
});
</script>函数式组件(无状态)
vue
<script setup>
// 纯展示组件,没有响应式状态
defineProps(['title', 'content']);
</script>
<template>
<div class="card">
<h2>{{ title }}</h2>
<p>{{ content }}</p>
</div>
</template>列表渲染优化
使用 key
vue
<template>
<!-- 好:使用唯一 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- 不好:使用 index 作为 key -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
</template>虚拟滚动
vue
<template>
<!-- 使用 vue-virtual-scroller -->
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
})));
</script>分页加载
vue
<script setup>
import { ref, onMounted } from 'vue';
const items = ref([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
async function loadMore() {
if (loading.value || !hasMore.value) return;
loading.value = true;
const newItems = await fetchItems(page.value);
if (newItems.length === 0) {
hasMore.value = false;
} else {
items.value.push(...newItems);
page.value++;
}
loading.value = false;
}
// 使用 IntersectionObserver 实现无限滚动
const sentinel = ref(null);
onMounted(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (sentinel.value) {
observer.observe(sentinel.value);
}
});
</script>
<template>
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<div ref="sentinel" v-show="hasMore">加载中...</div>
</template>Watch 优化
精确监听
javascript
import { watch, ref } from 'vue';
const state = ref({
user: { name: 'John', profile: { age: 25 } },
settings: { theme: 'dark' }
});
// 不好:监听整个对象
watch(state, () => {
console.log('state changed');
}, { deep: true });
// 好:只监听需要的部分
watch(
() => state.value.user.name,
(newName) => {
console.log('name changed:', newName);
}
);使用 watchEffect 的清理函数
javascript
import { watchEffect, ref } from 'vue';
const id = ref(1);
watchEffect((onCleanup) => {
const controller = new AbortController();
fetch(`/api/data/${id.value}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data));
// 清理函数:在下次执行前或组件卸载时调用
onCleanup(() => {
controller.abort();
});
});延迟执行
javascript
import { watch, ref } from 'vue';
const searchQuery = ref('');
// 防抖
let timeout;
watch(searchQuery, (value) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
performSearch(value);
}, 300);
});
// 或使用 flush: 'post'
watch(source, callback, {
flush: 'post' // DOM 更新后执行
});构建优化
Tree-shaking
javascript
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus']
}
}
}
}
};按需导入
javascript
// 不好:全量导入
import ElementPlus from 'element-plus';
app.use(ElementPlus);
// 好:按需导入
import { ElButton, ElInput } from 'element-plus';
app.component('ElButton', ElButton);
app.component('ElInput', ElInput);
// 使用 unplugin-vue-components 自动导入
// vite.config.js
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
Components({
resolvers: [ElementPlusResolver()]
})
]
};代码分割
javascript
// 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
},
{
path: '/settings',
component: () => import('./views/Settings.vue')
}
];
// 带注释的分组
const routes = [
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ './views/Admin.vue')
}
];性能监控
使用 Vue Devtools
javascript
// 开启性能追踪
app.config.performance = true;
// 在 Chrome Performance 面板中可以看到组件渲染时间自定义性能监控
javascript
import { onMounted, onUpdated, getCurrentInstance } from 'vue';
// 组件渲染时间
function useRenderTime() {
const instance = getCurrentInstance();
let startTime;
onMounted(() => {
const renderTime = performance.now() - startTime;
console.log(`${instance?.type.name} mounted in ${renderTime}ms`);
});
onUpdated(() => {
const updateTime = performance.now() - startTime;
console.log(`${instance?.type.name} updated in ${updateTime}ms`);
});
// 在 setup 开始时记录
startTime = performance.now();
}使用 Performance API
javascript
// 标记和测量
function measureComponent(name) {
return {
start() {
performance.mark(`${name}-start`);
},
end() {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measures = performance.getEntriesByName(name);
console.log(`${name}: ${measures[0].duration}ms`);
// 清理
performance.clearMarks();
performance.clearMeasures();
}
};
}面试常见问题
1. Vue 3 相比 Vue 2 有哪些性能提升?
- 编译优化:静态提升、PatchFlags、事件缓存
- 响应式优化:Proxy 替代 Object.defineProperty
- Tree-shaking:按需打包,减小体积
- Fragment:减少无意义的根节点
- Teleport:更高效的弹窗渲染
2. 什么时候使用 shallowRef?
- 大型对象或数组,内部数据变化频繁但不需要细粒度响应
- 第三方库实例
- 性能敏感场景,手动控制更新
3. v-memo 的使用场景?
- 大列表中每项包含复杂子组件
- 条件渲染优化(如选中状态)
- 需要精确控制重渲染的场景
4. 如何优化大列表渲染?
javascript
// 1. 虚拟滚动
// 2. 分页加载
// 3. v-memo 缓存
// 4. shallowRef 减少响应式开销
// 5. 使用 key 优化 diff5. computed 和 watch 的性能差异?
- computed:有缓存,依赖不变不重新计算
- watch:每次依赖变化都执行
- 优先使用 computed,需要副作用时使用 watch
总结
Vue 3 性能优化的核心策略:
- 响应式优化:shallowRef、markRaw、避免不必要的响应式
- 组件优化:v-once、v-memo、异步组件、KeepAlive
- 列表优化:虚拟滚动、分页加载、正确使用 key
- 计算优化:合理使用 computed、精确监听
- 构建优化:Tree-shaking、按需导入、代码分割
掌握这些优化技巧,能够构建高性能的 Vue 3 应用。