性能优化
Vue 3 提供了多种性能优化手段,从响应式系统到编译时优化,再到运行时和打包优化。本章节涵盖完整的性能优化策略。
性能优化体系
code
┌─────────────────────────────────────────────────────────────────────┐
│ Vue 3 性能优化体系 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 编译时优化 │ │ 运行时优化 │ │ 打包优化 │ │
│ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
│ │ • 静态提升 │ │ • 响应式优化 │ │ • Tree Shaking │ │
│ │ • Patch Flags │ │ • 组件缓存 │ │ • 代码分割 │ │
│ │ • 预渲染 │ │ • 虚拟列表 │ │ • 懒加载 │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ 性能监控 │ │
│ │ • DevTools │ │
│ │ • Performance API │ │
│ │ • Lighthouse │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘响应式优化
使用 shallowRef/shallowReactive
对于大型对象,深层响应式会带来性能开销。
ts
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// ✅ 推荐:大型不可变数据使用 shallowRef
const largeList = shallowRef<BigData[]>([])
// 更新时替换整个数组
largeList.value = newData
// 如果需要强制触发更新
triggerRef(largeList)
// ✅ 推荐:配置对象使用 shallowReactive
const config = shallowReactive({
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
features: {
darkMode: true,
notifications: false
}
})
// 只有顶层属性是响应式的
config.api = newApiConfig // ✅ 触发更新
config.api.timeout = 3000 // ❌ 不触发更新避免不必要的响应式
ts
import { markRaw, ref, reactive } from 'vue'
// ✅ 推荐:静态配置使用 markRaw
const constants = markRaw({
API_URL: 'https://api.example.com',
MAX_ITEMS: 100,
VERSION: '1.0.0'
})
// ✅ 第三方库实例使用 markRaw
const chartInstance = markRaw(new Chart(ctx, config))
// ✅ 纯数据展示不需要响应式
interface TableData {
id: number
name: string
value: number
}
// 如果数据只用于展示,不需要响应式
const tableData = ref<TableData[]>([])
tableData.value = await fetchData() // 获取后直接赋值
// ❌ 不推荐:对静态数据创建响应式
const staticOptions = reactive({
items: ['a', 'b', 'c'] // 永远不会变化的数据不需要响应式
})computed vs 方法
Vue SFC
<script setup>
import { ref, computed } from 'vue'
const list = ref([1, 2, 3, 4, 5])
// ✅ 推荐:有缓存,适合模板中多次使用
const filteredList = computed(() => list.value.filter(n => n > 2))
// ✅ 推荐:需要传参时使用方法
function filterByMin(min: number) {
return list.value.filter(n => n >= min)
}
// 场景对比
const expensiveValue = computed(() => {
// 复杂计算会被缓存
return list.value.reduce((sum, n) => sum + n * n, 0)
})
</script>
<template>
<!-- computed 会被缓存 -->
<div>Filtered: {{ filteredList }}</div>
<div>Filtered again: {{ filteredList }}</div>
<!-- 方法每次都会执行 -->
<div>Min 3: {{ filterByMin(3) }}</div>
</template>使用 toRef/toRefs 解构
ts
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({
count: 0,
name: 'Vue'
})
// ✅ 推荐:使用 toRefs 保持响应式
const { count, name } = toRefs(state)
// ✅ 单个属性使用 toRef
const countRef = toRef(state, 'count')
// ❌ 直接解构会失去响应式
const { count: lostCount } = state // 不是响应式组件优化
使用 v-memo 缓存
Vue SFC
<template>
<!-- v-memo 接收依赖数组,只有依赖变化时才重新渲染 -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.selected, item.count]"
>
<!-- 只有 selected 或 count 变化时才更新 -->
<span>{{ item.name }}</span>
<span :class="{ active: item.selected }">
{{ item.count }}
</span>
</div>
<!-- 结合 v-once 处理完全静态的内容 -->
<div v-for="item in staticItems" :key="item.id" v-memo="[]">
<!-- 永远不会更新 -->
{{ item.name }}
</div>
</template>使用 v-once
Vue SFC
<template>
<!-- 只渲染一次,后续不再更新 -->
<div v-once>
<h1>{{ staticTitle }}</h1>
<p>{{ staticDescription }}</p>
</div>
<!-- 适用于静态列表、图标等 -->
<div v-for="icon in icons" :key="icon" v-once>
<Icon :name="icon" />
</div>
</template>合理使用 KeepAlive
Vue SFC
<script setup>
import { ref } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'
const currentTab = ref('TabA')
</script>
<template>
<!-- 缓存组件状态,避免重复渲染 -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>
<!-- 配置包含/排除 -->
<KeepAlive :include="['TabA', 'TabB']" :exclude="['TabC']">
<component :is="currentTab" />
</KeepAlive>
<!-- 配置最大缓存数 -->
<KeepAlive :max="10">
<component :is="currentTab" />
</KeepAlive>组件懒加载
Vue SFC
<script setup>
import { defineAsyncComponent, ref } from 'vue'
// 异步组件定义
const AsyncModal = defineAsyncComponent(() =>
import('./HeavyModal.vue')
)
// 带配置的异步组件
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
const showModal = ref(false)
</script>
<template>
<button @click="showModal = true">打开弹窗</button>
<!-- 条件渲染时才加载 -->
<AsyncModal v-if="showModal" @close="showModal = false" />
</template>异步组件最佳实践
ts
// components/index.ts - 组件预加载策略
import { defineAsyncComponent } from 'vue'
// 关键路径组件 - 立即加载
import Header from './Header.vue'
import Sidebar from './Sidebar.vue'
// 非关键组件 - 按需加载
const UserPanel = defineAsyncComponent(() => import('./UserPanel.vue'))
const SearchModal = defineAsyncComponent(() => import('./SearchModal.vue'))
// 预加载策略:空闲时预加载可能需要的组件
const preloadComponents = () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./UserPanel.vue')
import('./SearchModal.vue')
})
}
}
// 路由切换时预加载
router.beforeEach((to, from, next) => {
const component = to.matched[0]?.components?.default
if (typeof component === 'function') {
component() // 预加载组件
}
next()
})编译优化
使用 <script setup>
Vue SFC
<!-- ✅ 推荐:<script setup> 有更好的编译优化 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- 编译后生成更简洁的代码 -->静态提升
Vue 3 自动将静态内容提升到渲染函数外:
Vue SFC
<template>
<!-- 静态节点会被提升,只创建一次 -->
<div class="header">
<h1>Static Title</h1>
</div>
<!-- 动态节点正常处理 -->
<div class="content">
{{ dynamicContent }}
</div>
</template>Patch Flags
Vue 3 使用 Patch Flags 标记动态内容,精确更新:
Vue SFC
<template>
<!-- 只有 text 需要更新 -->
<div>{{ message }}</div>
<!-- 只有 class 需要更新 -->
<div :class="{ active: isActive }">Static</div>
<!-- 只有 props 需要更新 -->
<div :id="dynamicId">Static</div>
</template>合理使用模板
Vue SFC
<template>
<!-- ✅ 推荐:使用 <template> 避免额外 DOM 节点 -->
<template v-for="item in items" :key="item.id">
<li>{{ item.name }}</li>
<li>{{ item.desc }}</li>
</template>
<!-- ❌ 不推荐:多余的包装元素 -->
<div v-for="item in items" :key="item.id">
<li>{{ item.name }}</li>
<li>{{ item.desc }}</li>
</div>
</template>打包优化
路由懒加载
ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // 懒加载
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import(
/* webpackChunkName: "dashboard" */
'@/views/Dashboard.vue'
),
children: [
{
path: 'analytics',
component: () => import(
/* webpackChunkName: "dashboard" */
'@/views/DashboardAnalytics.vue'
)
}
]
}
]
})代码分割策略
ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
// 第三方库分离
'vendor': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus', '@element-plus/icons-vue'],
'utils': ['lodash-es', 'dayjs'],
// 业务模块分离
'dashboard': [
'./src/views/Dashboard.vue',
'./src/views/DashboardAnalytics.vue'
]
}
}
},
// 调整 chunk 大小警告阈值
chunkSizeWarningLimit: 1000
}
})动态导入策略
ts
// 按功能模块分割
const loadModule = async (moduleName: string) => {
const modules = {
chart: () => import('./modules/chart'),
table: () => import('./modules/table'),
form: () => import('./modules/form')
}
return modules[moduleName]?.()
}
// 条件加载
const loadPolyfills = async () => {
if (!('IntersectionObserver' in window)) {
await import('intersection-observer')
}
}Tree Shaking 优化
ts
// ✅ 推荐:具名导入,支持 Tree Shaking
import { ref, computed, watch } from 'vue'
import { debounce, throttle } from 'lodash-es'
// ❌ 不推荐:默认导入整个库
import _ from 'lodash-es'
// ✅ Element Plus 按需导入
import { ElButton, ElInput } from 'element-plus'
// 或使用 unplugin-vue-components 自动导入
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
Components({
resolvers: [ElementPlusResolver()]
})
]
}渲染优化
虚拟列表
Vue SFC
<script setup>
import { ref } from 'vue'
import { useVirtualList } from '@vueuse/core'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
const { list, containerProps, wrapperProps } = useVirtualList(
items,
{ itemHeight: 50 }
)
</script>
<template>
<div v-bind="containerProps" style="height: 500px; overflow-y: auto;">
<div v-bind="wrapperProps">
<div
v-for="{ data, index } in list"
:key="data.id"
style="height: 50px;"
>
{{ index }}: {{ data.text }}
</div>
</div>
</div>
</template>使用 requestAnimationFrame
ts
import { ref, onUnmounted } from 'vue'
function useAnimatedCounter(target: number, duration: number = 1000) {
const count = ref(0)
let animationFrameId: number | null = null
const animate = (startTime: number) => {
const elapsed = performance.now() - startTime
const progress = Math.min(elapsed / duration, 1)
count.value = Math.floor(progress * target)
if (progress < 1) {
animationFrameId = requestAnimationFrame(() => animate(startTime))
}
}
const start = () => {
const startTime = performance.now()
animate(startTime)
}
onUnmounted(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
})
return { count, start }
}防抖与节流
Vue SFC
<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'
const searchQuery = ref('')
// 防抖:延迟执行
const debouncedSearch = useDebounceFn((query: string) => {
// 执行搜索
console.log('Searching:', query)
}, 300)
// 节流:固定间隔执行
const throttledScroll = useThrottleFn(() => {
// 处理滚动
console.log('Scroll position:', window.scrollY)
}, 100)
// 输入处理
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
searchQuery.value = value
debouncedSearch(value)
}
</script>网络优化
数据缓存
ts
import { ref, shallowRef } from 'vue'
// 简单缓存实现
const cache = new Map<string, { data: unknown; timestamp: number }>()
async function useCachedFetch<T>(
url: string,
ttl: number = 5 * 60 * 1000 // 5分钟
): Promise<T> {
const cached = cache.get(url)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data as T
}
const response = await fetch(url)
const data = await response.json()
cache.set(url, { data, timestamp: Date.now() })
return data
}
// 组件中使用
const userData = shallowRef<User | null>(null)
async function fetchUser(id: number) {
userData.value = await useCachedFetch<User>(`/api/users/${id}`)
}请求并行
ts
// ✅ 推荐:并行请求
async function loadDashboardData() {
const [users, stats, notifications] = await Promise.all([
fetchUsers(),
fetchStats(),
fetchNotifications()
])
return { users, stats, notifications }
}
// 带错误处理的并行请求
async function loadDashboardDataSafe() {
const results = await Promise.allSettled([
fetchUsers(),
fetchStats(),
fetchNotifications()
])
return {
users: results[0].status === 'fulfilled' ? results[0].value : [],
stats: results[1].status === 'fulfilled' ? results[1].value : null,
notifications: results[2].status === 'fulfilled' ? results[2].value : []
}
}性能监控
Vue DevTools
ts
// 在开发环境启用性能追踪
if (import.meta.env.DEV) {
// 组件渲染性能
app.config.performance = true
}Performance API
ts
// 性能标记
function measureComponentRender(componentName: string) {
const startMark = `${componentName}-start`
const endMark = `${componentName}-end`
performance.mark(startMark)
return () => {
performance.mark(endMark)
performance.measure(componentName, startMark, endMark)
const measure = performance.getEntriesByName(componentName)[0]
console.log(`${componentName} render time: ${measure.duration}ms`)
performance.clearMarks(startMark)
performance.clearMarks(endMark)
performance.clearMeasures(componentName)
}
}
// 使用
const endMeasure = measureComponentRender('UserList')
// ... 渲染逻辑
endMeasure()自定义性能钩子
ts
// composables/usePerformance.ts
import { onMounted, onUpdated } from 'vue'
export function usePerformanceMonitor(componentName: string) {
let startTime: number
const logRenderTime = (phase: string) => {
const duration = performance.now() - startTime
if (duration > 16) { // 超过一帧(60fps)
console.warn(
`[Performance] ${componentName} ${phase} took ${duration.toFixed(2)}ms`
)
}
}
onMounted(() => {
startTime = performance.now()
logRenderTime('mount')
})
onUpdated(() => {
startTime = performance.now()
logRenderTime('update')
})
}Web Vitals 监控
ts
// utils/webVitals.ts
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals'
export function initWebVitals() {
onCLS(console.log) // Cumulative Layout Shift
onFID(console.log) // First Input Delay
onLCP(console.log) // Largest Contentful Paint
onFCP(console.log) // First Contentful Paint
onTTFB(console.log) // Time to First Byte
// 发送到分析服务
const sendToAnalytics = (metric: Metric) => {
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id
})
})
}
}性能检查清单
开发阶段
- 使用
shallowRef/shallowReactive处理大型数据 - 列表渲染使用唯一
key - 避免在模板中使用复杂表达式
- 使用
computed缓存计算结果 - 合理使用
v-memo和v-once
构建阶段
- 配置路由懒加载
- 第三方库按需导入
- 配置合理的代码分割策略
- 启用 Gzip/Brotli 压缩
- 配置资源缓存策略
运行阶段
- 大列表使用虚拟滚动
- 图片懒加载
- 防抖节流高频操作
- 避免内存泄漏(清理副作用)