跳到主要内容

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/virtualvue-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: 如何优化长列表性能?

答案

  1. 虚拟列表:只渲染可见区域
  2. 分页/无限滚动:按需加载数据
  3. v-memo:缓存列表项
  4. 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 的性能区别?

答案

特性computedmethod
缓存✅ 依赖不变时使用缓存❌ 每次调用都执行
调用方式属性访问函数调用
适用场景派生数据事件处理、有副作用
// 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 层级

相关链接