Vue 性能优化
问题
Vue 应用有哪些常见的性能问题?如何优化?
答案
Vue 性能优化可以从编译时、运行时和加载时三个维度进行。
性能优化全景图
运行时优化
1. 减少不必要的重渲染
使用 v-memo
v-memo 可以缓存模板的一部分,只有依赖变化时才重新渲染:
<template>
<!-- 只有 id 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
<span>{{ item.name }}</span>
<span>{{ item.description }}</span>
<!-- 复杂渲染逻辑 -->
</div>
</template>
使用 shallowRef / shallowReactive
对于大型对象,使用浅层响应式避免深层追踪:
import { shallowRef, shallowReactive, triggerRef } from 'vue';
// 深层响应式(默认)- 每个属性都被追踪
const deepState = ref({
nested: { deep: { value: 1 } }
});
// 浅层响应式 - 只追踪第一层
const shallowState = shallowRef({
nested: { deep: { value: 1 } }
});
// 修改深层属性不会触发更新
shallowState.value.nested.deep.value = 2; // ❌ 不触发
// 手动触发更新
triggerRef(shallowState);
// 或替换整个值
shallowState.value = { ...shallowState.value }; // ✅ 触发
使用 markRaw
标记对象不需要响应式:
import { markRaw, reactive } from 'vue';
const heavyObject = markRaw({
// 大量数据,不需要响应式
items: [...Array(10000).keys()].map(i => ({ id: i }))
});
const state = reactive({
heavyObject // 不会被转为响应式
});
2. 优化组件渲染
使用 v-once
只渲染一次,后续更新跳过:
<template>
<!-- 静态内容,只渲染一次 -->
<div v-once>
<h1>{{ staticTitle }}</h1>
<p>{{ staticDescription }}</p>
</div>
</template>
合理拆分组件
<!-- ❌ 大组件:任何状态变化都导致整体重渲染 -->
<template>
<div>
<header>{{ title }}</header>
<ExpensiveChart :data="chartData" />
<footer>{{ footer }}</footer>
</div>
</template>
<!-- ✅ 拆分组件:隔离重渲染 -->
<template>
<div>
<AppHeader :title="title" />
<ChartWrapper :data="chartData" />
<AppFooter :footer="footer" />
</div>
</template>
使用函数式组件
对于无状态的展示组件:
<!-- 函数式组件:没有实例,更轻量 -->
<script setup>
// 函数式组件在 Vue 3 中就是普通的 setup 组件
// 如果没有状态和生命周期,就是"函数式"的
const props = defineProps(['item']);
</script>
<template>
<div class="item">{{ item.name }}</div>
</template>
3. 列表渲染优化
使用唯一稳定的 key
<template>
<!-- ✅ 使用唯一 ID -->
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
<!-- ❌ 避免 index 作为 key -->
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
</template>
虚拟列表
对于大量数据,只渲染可见区域:
<template>
<div class="virtual-list" @scroll="handleScroll" ref="container">
<div class="spacer" :style="{ height: totalHeight + 'px' }"></div>
<div class="visible-items" :style="{ transform: `translateY(${offset}px)` }">
<div v-for="item in visibleItems" :key="item.id" class="item">
{{ item.name }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const props = defineProps<{
items: { id: number; name: string }[];
itemHeight: number;
containerHeight: number;
}>();
const scrollTop = ref(0);
const container = ref<HTMLElement>();
const totalHeight = computed(() => props.items.length * props.itemHeight);
const startIndex = computed(() =>
Math.floor(scrollTop.value / props.itemHeight)
);
const endIndex = computed(() =>
Math.min(
startIndex.value + Math.ceil(props.containerHeight / props.itemHeight) + 1,
props.items.length
)
);
const visibleItems = computed(() =>
props.items.slice(startIndex.value, endIndex.value)
);
const offset = computed(() => startIndex.value * props.itemHeight);
function handleScroll() {
scrollTop.value = container.value?.scrollTop ?? 0;
}
</script>
推荐使用成熟库:@vueuse/virtual、vue-virtual-scroller。
4. 异步组件与懒加载
import { defineAsyncComponent } from 'vue';
// 基础用法
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
// 带配置
const AsyncWithConfig = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 延迟显示 loading
timeout: 10000 // 超时时间
});
// 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
];
5. 缓存策略
keep-alive 缓存
<template>
<!-- 缓存组件,避免重复渲染 -->
<keep-alive :max="10">
<component :is="currentTab" />
</keep-alive>
<!-- 配合路由 -->
<router-view v-slot="{ Component }">
<keep-alive :include="['ListView', 'SearchView']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
computed 缓存
// ✅ computed 自动缓存
const expensiveValue = computed(() => {
return heavyCalculation(props.data);
});
// ❌ 每次渲染都重新计算
const expensiveValue = () => heavyCalculation(props.data);
加载时优化
1. 代码分割
// 按路由分割
const routes = [
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ './Admin.vue')
},
{
path: '/user',
component: () => import(/* webpackChunkName: "user" */ './User.vue')
}
];
// 按功能分割
const HeavyChart = defineAsyncComponent(() =>
import(/* webpackChunkName: "charts" */ './HeavyChart.vue')
);
2. 预加载与预获取
// 预获取(prefetch)- 空闲时加载
const AdminPanel = defineAsyncComponent(() =>
import(/* webpackPrefetch: true */ './AdminPanel.vue')
);
// 预加载(preload)- 立即加载
const CriticalComponent = defineAsyncComponent(() =>
import(/* webpackPreload: true */ './CriticalComponent.vue')
);
3. Tree Shaking
// ✅ 只导入需要的
import { ref, computed, watch } from 'vue';
// ❌ 导入整个库
import * as Vue from 'vue';
// ✅ 按需导入组件库
import { Button, Input } from 'element-plus';
// ❌ 全量导入
import ElementPlus from 'element-plus';
4. 图片优化
<template>
<!-- 懒加载图片 -->
<img v-lazy="imageSrc" />
<!-- 响应式图片 -->
<picture>
<source media="(min-width: 800px)" :srcset="largeImage" />
<source media="(min-width: 400px)" :srcset="mediumImage" />
<img :src="smallImage" loading="lazy" />
</picture>
</template>
性能分析工具
Vue DevTools
// 在开发环境启用性能追踪
app.config.performance = true;
// 然后在 DevTools 的 Performance 面板查看组件渲染时间
自定义性能标记
import { onRenderTracked, onRenderTriggered } from 'vue';
// 调试渲染
onRenderTracked((e) => {
console.log('依赖被追踪:', e);
});
onRenderTriggered((e) => {
console.log('触发重渲染:', e);
});
常见面试问题
Q1: Vue 应用首屏加载慢如何优化?
答案:
| 优化方向 | 具体措施 |
|---|---|
| 减少体积 | Tree Shaking、代码分割、压缩、Gzip |
| 加快加载 | CDN、HTTP/2、预加载关键资源 |
| 优化渲染 | 骨架屏、SSR/SSG、关键 CSS 内联 |
| 延迟加载 | 路由懒加载、图片懒加载、异步组件 |
// 路由懒加载
const routes = [
{ path: '/', component: () => import('./Home.vue') }
];
// 组件懒加载
const HeavyComponent = defineAsyncComponent(() =>
import('./Heavy.vue')
);
// 第三方库 CDN
// vite.config.ts
export default {
build: {
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
}
}
}
};
Q2: 如何优化长列表性能?
答案:
- 虚拟列表:只渲染可见区域
- 分页/无限滚动:按需加载数据
- v-memo:缓存列表项
- Object.freeze:冻结不变的数据
// 使用 @vueuse/virtual
import { useVirtualList } from '@vueuse/core';
const { list, containerProps, wrapperProps } = useVirtualList(
items,
{ itemHeight: 40 }
);
// 冻结大数据
const frozenList = Object.freeze(largeArray.map(item => Object.freeze(item)));
Q3: computed 和 method 的性能区别?
答案:
| 特性 | computed | method |
|---|---|---|
| 缓存 | ✅ 依赖不变时使用缓存 | ❌ 每次调用都执行 |
| 调用方式 | 属性访问 | 函数调用 |
| 适用场景 | 派生数据 | 事件处理、有副作用 |
// computed:只在 count 变化时重新计算
const double = computed(() => count.value * 2);
// method:每次渲染都执行
function getDouble() {
return count.value * 2;
}
// 模板中
// {{ double }} - 使用缓存
// {{ getDouble() }} - 每次渲染都调用
Q4: 如何减少组件重渲染?
答案:
<script setup lang="ts">
import { shallowRef, computed } from 'vue';
// 1. 使用 shallowRef(大对象)
const bigData = shallowRef({ /* 大量数据 */ });
// 2. 使用 computed 缓存
const processedData = computed(() => expensiveCalculation(props.data));
// 3. 合理拆分组件
// 将频繁变化的部分独立成子组件
// 4. 使用 v-once(静态内容)
// <div v-once>{{ staticContent }}</div>
// 5. 使用 v-memo(列表项)
// <div v-for="item in list" :key="item.id" v-memo="[item.id]">
</script>
Q5: Vue 3 相比 Vue 2 有哪些性能提升?
答案:
| 优化 | 说明 | 效果 |
|---|---|---|
| Proxy 响应式 | 不需要递归遍历 | 初始化更快 |
| 静态提升 | 静态节点只创建一次 | 减少内存分配 |
| PatchFlags | 标记动态内容 | Diff 更精确 |
| Block Tree | 收集动态节点 | 跳过静态子节点 |
| 事件缓存 | 缓存事件处理函数 | 减少子组件重渲染 |
| Tree Shaking | 按需打包 | 减小体积 |
| Fragment | 无需根节点 | 减少 DOM 层级 |