Vue 3 在性能方面相比 Vue 2 有了显著提升,但在实际开发中,我们仍需要掌握各种性能优化技巧来构建高性能的应用。本文将从 Vue 3 的性能改进原理出发,深入探讨实际项目中的性能优化策略和最佳实践。
Vue 3 性能提升的核心原理
1. 编译时优化
静态标记(PatchFlag)
Vue 3 在编译阶段会为动态节点添加静态标记,运行时只需要更新标记的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <div> <span>{{ message }}</span> <p>静态文本</p> </div>
function render() { return createVNode('div', null, [ createVNode('span', null, message, 1 ), createVNode('p', null, '静态文本') ]) }
|
静态提升(hoistStatic)
静态元素会被提升到渲染函数外部,避免重复创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function render() { return createVNode('div', null, [ createVNode('h1', null, '标题'), createVNode('p', null, message) ]) }
const _hoisted_1 = createVNode('h1', null, '标题')
function render() { return createVNode('div', null, [ _hoisted_1, createVNode('p', null, message) ]) }
|
2. 运行时优化
Proxy 响应式系统
Vue 3 使用 Proxy 替代 Object.defineProperty,支持更多数据类型的响应式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const data = { items: [] }
data.items[0] = newItem data.items.length = 0 data.newProperty = 'value'
const state = reactive({ items: [], map: new Map(), set: new Set() })
state.items[0] = newItem state.items.length = 0 state.newProperty = 'value' state.map.set('key', 'value') state.set.add('item')
|
组件级性能优化
1. 合理使用 v-if 和 v-show
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <!-- 频繁切换使用 v-show --> <div v-show="isVisible" class="modal"> 模态框内容 </div> <!-- 条件渲染使用 v-if --> <div v-if="userRole === 'admin'" class="admin-panel"> 管理员面板 </div> <!-- 复杂组件的条件渲染 --> <template v-if="shouldRenderChart"> <heavy-chart-component :data="chartData" /> </template> </template>
<script setup> import { computed } from 'vue'
const props = defineProps(['userRole', 'chartData'])
// 使用计算属性缓存复杂的条件判断 const shouldRenderChart = computed(() => { return props.chartData && props.chartData.length > 0 && props.userRole === 'admin' }) </script>
|
2. 使用 KeepAlive 缓存组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <template> <div> <!-- 缓存路由组件 --> <router-view v-slot="{ Component }"> <keep-alive :include="cachedViews" :max="10"> <component :is="Component" :key="$route.fullPath" /> </keep-alive> </router-view> <!-- 缓存标签页组件 --> <div class="tabs"> <button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="{ active: activeTab === tab.id }" > {{ tab.name }} </button> </div> <keep-alive> <component :is="currentTabComponent" :data="tabData" /> </keep-alive> </div> </template>
<script setup> import { ref, computed } from 'vue' import TabA from './TabA.vue' import TabB from './TabB.vue' import TabC from './TabC.vue'
const activeTab = ref('a') const cachedViews = ref(['ProductList', 'UserProfile'])
const tabs = [ { id: 'a', name: '标签A', component: TabA }, { id: 'b', name: '标签B', component: TabB }, { id: 'c', name: '标签C', component: TabC } ]
const currentTabComponent = computed(() => { return tabs.find(tab => tab.id === activeTab.value)?.component }) </script>
|
3. 优化大列表渲染
虚拟滚动实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| <template> <div class="virtual-list" @scroll="handleScroll" ref="containerRef"> <div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div> <div class="virtual-list-content" :style="{ transform: `translateY(${offsetY}px)` }"> <div v-for="item in visibleItems" :key="item.id" class="virtual-list-item" :style="{ height: itemHeight + 'px' }" > <slot :item="item" :index="item.index"> {{ item.text }} </slot> </div> </div> </div> </template>
<script setup> import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({ items: { type: Array, required: true }, itemHeight: { type: Number, default: 50 }, containerHeight: { type: Number, default: 400 } })
const containerRef = ref(null) const scrollTop = ref(0)
// 计算总高度 const totalHeight = computed(() => props.items.length * props.itemHeight)
// 计算可见区域的起始和结束索引 const startIndex = computed(() => { return Math.floor(scrollTop.value / props.itemHeight) })
const endIndex = computed(() => { const visibleCount = Math.ceil(props.containerHeight / props.itemHeight) return Math.min(startIndex.value + visibleCount + 1, props.items.length) })
// 计算可见项目 const visibleItems = computed(() => { return props.items.slice(startIndex.value, endIndex.value).map((item, index) => ({ ...item, index: startIndex.value + index })) })
// 计算偏移量 const offsetY = computed(() => startIndex.value * props.itemHeight)
// 滚动事件处理 function handleScroll(event) { scrollTop.value = event.target.scrollTop }
// 防抖优化 let ticking = false function optimizedHandleScroll(event) { if (!ticking) { requestAnimationFrame(() => { handleScroll(event) ticking = false }) ticking = true } }
onMounted(() => { if (containerRef.value) { containerRef.value.addEventListener('scroll', optimizedHandleScroll, { passive: true }) } })
onUnmounted(() => { if (containerRef.value) { containerRef.value.removeEventListener('scroll', optimizedHandleScroll) } }) </script>
<style scoped> .virtual-list { height: 400px; overflow-y: auto; position: relative; }
.virtual-list-phantom { position: absolute; left: 0; top: 0; right: 0; z-index: -1; }
.virtual-list-content { left: 0; right: 0; top: 0; position: absolute; }
.virtual-list-item { padding: 10px; border-bottom: 1px solid #eee; box-sizing: border-box; } </style>
|
使用虚拟滚动组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <div> <VirtualList :items="largeDataset" :item-height="60" :container-height="500" > <template #default="{ item }"> <div class="user-item"> <img : src="item.avatar" :alt="item.name" class="avatar"> <div class="user-info"> <h3>{{ item.name }}</h3> <p>{{ item.email }}</p> </div> </div> </template> </VirtualList> </div> </template>
<script setup> import { ref, onMounted } from 'vue' import VirtualList from './VirtualList.vue'
const largeDataset = ref([])
// 模拟大量数据 onMounted(() => { largeDataset.value = Array.from({ length: 10000 }, (_, index) => ({ id: index, name: `用户 ${index}`, email: `user${index}@example.com`, avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${index}` })) }) </script>
|
异步组件和代码分割
1. 路由级代码分割
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { createRouter, createWebHistory } from 'vue-router'
const routes = [ { path: '/', name: 'Home', component: () => import('../views/Home.vue') }, { path: '/products', name: 'Products', component: () => import('../views/Products.vue') }, { path: '/admin', name: 'Admin', component: () => import('../views/Admin.vue'), meta: { requiresAuth: true } } ]
const router = createRouter({ history: createWebHistory(), routes })
export default router
|
2. 组件级懒加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <template> <div> <button @click="showChart = true" v-if="!showChart"> 显示图表 </button> <!-- 懒加载重型组件 --> <Suspense v-if="showChart"> <template #default> <AsyncChart :data="chartData" /> </template> <template #fallback> <div class="loading">加载图表中...</div> </template> </Suspense> <!-- 条件懒加载 --> <Suspense v-if="userRole === 'admin'"> <template #default> <AdminPanel /> </template> <template #fallback> <div class="loading">加载管理面板中...</div> </template> </Suspense> </div> </template>
<script setup> import { ref, defineAsyncComponent } from 'vue'
const showChart = ref(false) const userRole = ref('user')
// 异步组件定义 const AsyncChart = defineAsyncComponent({ loader: () => import('./HeavyChart.vue'), loadingComponent: () => import('./LoadingSpinner.vue'), errorComponent: () => import('./ErrorMessage.vue'), delay: 200, timeout: 3000 })
const AdminPanel = defineAsyncComponent(() => import('./AdminPanel.vue')) </script>
|
3. 图片懒加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| <template> <div class="image-gallery"> <div v-for="image in images" :key="image.id" class="image-container" ref="imageRefs" > <img v-if="image.loaded" : src="image.src" :alt="image.alt" class="lazy-image" @load="onImageLoad(image)" @error="onImageError(image)" > <div v-else class="image-placeholder"> <div class="loading-spinner"></div> </div> </div> </div> </template>
<script setup> import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({ images: { type: Array, required: true } })
const imageRefs = ref([]) const images = ref(props.images.map(img => ({ ...img, loaded: false })))
let observer = null
// 图片加载成功 function onImageLoad(image) { console.log(`图片加载成功: ${image.alt}`) }
// 图片加载失败 function onImageError(image) { console.error(`图片加载失败: ${image.alt}`) image.src = '/placeholder-error.jpg' // 设置错误占位图 }
// 创建 Intersection Observer function createObserver() { observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const index = imageRefs.value.indexOf(entry.target) if (index !== -1 && !images.value[index].loaded) { images.value[index].loaded = true observer.unobserve(entry.target) } } }) }, { rootMargin: '50px 0px', // 提前50px开始加载 threshold: 0.1 } ) }
onMounted(() => { createObserver() // 观察所有图片容器 imageRefs.value.forEach(ref => { if (ref) observer.observe(ref) }) })
onUnmounted(() => { if (observer) { observer.disconnect() } }) </script>
<style scoped> .image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
.image-container { aspect-ratio: 1; border-radius: 8px; overflow: hidden; }
.lazy-image { width: 100%; height: 100%; object-fit: cover; }
.image-placeholder { width: 100%; height: 100%; background: #f0f0f0; display: flex; align-items: center; justify-content: center; }
.loading-spinner { width: 24px; height: 24px; border: 2px solid #e0e0e0; border-top: 2px solid #007bff; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
|
响应式数据优化
1. 合理使用 ref 和 reactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const count = ref(0) const message = ref('Hello') const isLoading = ref(false)
const user = reactive({ name: 'John', age: 30, preferences: { theme: 'dark', language: 'zh-CN' } })
const largeData = reactive({ items: new Array(10000).fill(0).map((_, i) => ({ id: i, value: Math.random() })) })
const largeData = { items: new Array(10000).fill(0).map((_, i) => ({ id: i, value: Math.random() })) } const filteredItems = ref([]) const currentPage = ref(1)
|
2. 使用 shallowRef 和 shallowReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { shallowRef, shallowReactive, triggerRef } from 'vue'
const largeList = shallowRef([])
function updateList(newList) { largeList.value = newList triggerRef(largeList) }
const config = shallowReactive({ api: { baseURL: 'https://api.example.com', timeout: 5000 }, ui: { theme: 'light', locale: 'zh-CN' } })
config.api = { baseURL: 'https://new-api.example.com', timeout: 3000 }
|
3. 使用 markRaw 标记非响应式数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { markRaw, reactive } from 'vue'
const state = reactive({ chart: markRaw(new Chart()), map: markRaw(new Map()), editor: markRaw(new Monaco.Editor()) })
const appConfig = reactive({ routes: markRaw([ ]), constants: markRaw({ }) })
|
计算属性和侦听器优化
1. 计算属性缓存优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import { computed, ref } from 'vue'
const items = ref([]) const searchQuery = ref('') const sortBy = ref('name') const filterCategory = ref('all')
const filteredItems = computed(() => { if (filterCategory.value === 'all') { return items.value } return items.value.filter(item => item.category === filterCategory.value) })
const searchedItems = computed(() => { if (!searchQuery.value) { return filteredItems.value } const query = searchQuery.value.toLowerCase() return filteredItems.value.filter(item => item.name.toLowerCase().includes(query) ) })
const sortedItems = computed(() => { return [...searchedItems.value].sort((a, b) => { const aValue = a[sortBy.value] const bValue = b[sortBy.value] return aValue > bValue ? 1 : -1 }) })
const processedItems = computed(() => { let result = items.value if (filterCategory.value !== 'all') { result = result.filter(item => item.category === filterCategory.value) } if (searchQuery.value) { const query = searchQuery.value.toLowerCase() result = result.filter(item => item.name.toLowerCase().includes(query)) } result = [...result].sort((a, b) => { const aValue = a[sortBy.value] const bValue = b[sortBy.value] return aValue > bValue ? 1 : -1 }) return result })
|
2. 侦听器性能优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import { watch, watchEffect, ref, nextTick } from 'vue'
const searchQuery = ref('') const searchResults = ref([])
let searchTimer = null watch(searchQuery, (newQuery) => { clearTimeout(searchTimer) searchTimer = setTimeout(async () => { if (newQuery.trim()) { searchResults.value = await searchAPI(newQuery) } else { searchResults.value = [] } }, 300) })
const userForm = ref({ name: '', email: '', preferences: { theme: 'light', notifications: true } })
watch(() => userForm.value.name, (newName) => { })
watch(() => userForm.value.preferences.theme, (newTheme) => { document.documentElement.setAttribute('data-theme', newTheme) })
watchEffect(() => { if (searchQuery.value && filterType.value) { performSearch(searchQuery.value, filterType.value) } })
const stopWatcher = watch(someRef, callback)
if (conditionMet) { stopWatcher() }
|
构建和打包优化
1. Vite 配置优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path'
export default defineConfig({ plugins: [vue()], build: { rollupOptions: { output: { manualChunks: { vue: ['vue', 'vue-router', 'pinia'], ui: ['element-plus', '@element-plus/icons-vue'], utils: ['lodash-es', 'dayjs', 'axios'] } } }, minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true } }, assetsInlineLimit: 4096 }, server: { hmr: { overlay: false } }, optimizeDeps: { include: [ 'vue', 'vue-router', 'pinia', 'element-plus', 'lodash-es' ] } })
|
2. 组件库按需引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], imports: ['vue', 'vue-router', 'pinia'] }), Components({ resolvers: [ElementPlusResolver()] }) ] })
|
性能监控和分析
1. 性能监控组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <template> <div> <div v-if="showPerformanceInfo" class="performance-info"> <p>组件渲染时间: {{ renderTime }}ms</p> <p>更新次数: {{ updateCount }}</p> </div> <slot /> </div> </template>
<script setup> import { ref, onMounted, onUpdated, onBeforeUpdate } from 'vue'
const showPerformanceInfo = ref(process.env.NODE_ENV === 'development') const renderTime = ref(0) const updateCount = ref(0) let startTime = 0
onBeforeUpdate(() => { startTime = performance.now() })
onUpdated(() => { renderTime.value = Math.round(performance.now() - startTime) updateCount.value++ })
onMounted(() => { // 监控长任务 if ('PerformanceObserver' in window) { const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.duration > 50) { console.warn(`长任务检测: ${entry.duration}ms`, entry) } }) }) observer.observe({ entryTypes: ['longtask'] }) } }) </script>
|
2. 内存泄漏检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { onUnmounted, ref } from 'vue'
export function useMemoryMonitor(componentName) { const memoryUsage = ref(0) let intervalId = null function checkMemory() { if (performance.memory) { memoryUsage.value = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) if (memoryUsage.value > 100) { console.warn(`${componentName} 内存使用过高: ${memoryUsage.value}MB`) } } } intervalId = setInterval(checkMemory, 5000) onUnmounted(() => { if (intervalId) { clearInterval(intervalId) } console.log(`${componentName} 组件已卸载`) }) return { memoryUsage } }
|
总结
Vue 3 的性能优化是一个系统性工程,需要从编译时优化、运行时优化、组件设计、数据管理等多个维度进行考虑。关键要点包括:
- 充分利用 Vue 3 的编译时优化:静态标记、静态提升等特性
- 合理使用响应式 API:根据数据特性选择 ref、reactive、shallowRef 等
- 组件级优化:虚拟滚动、懒加载、KeepAlive 缓存
- 代码分割和异步加载:路由级和组件级的按需加载
- 构建优化:合理的打包策略和资源优化
- 性能监控:建立完善的性能监控体系
在实际项目中,应该根据具体场景选择合适的优化策略,避免过度优化。记住,”过早的优化是万恶之源”,先确保功能正确,再针对性能瓶颈进行优化。通过合理的性能优化,可以显著提升 Vue 3 应用的用户体验和运行效率。