Appearance
Vue 3 性能优化技巧
Vue 3 在性能方面相比 Vue 2 有了显著提升,但合理的优化策略仍然是构建高性能应用的关键。本文将深入探讨 Vue 3 性能优化的各个方面,帮助你构建更快、更流畅的应用。
1. 编译时优化
1.1 静态提升(Static Hoisting)
Vue 3 编译器会自动将静态元素提升到渲染函数外部,避免重复创建。
vue
<template>
<div>
<!-- 静态内容会被提升 -->
<h1>静态标题</h1>
<p>静态段落</p>
<!-- 动态内容 -->
<span>{{ message }}</span>
</div>
</template>
<script>
// 编译后的优化代码(简化版)
const _hoisted_1 = /*#__PURE__*/ createElementVNode("h1", null, "静态标题")
const _hoisted_2 = /*#__PURE__*/ createElementVNode("p", null, "静态段落")
export default {
setup() {
return function render() {
return createElementVNode("div", null, [
_hoisted_1, // 复用静态节点
_hoisted_2,
createElementVNode("span", null, message.value)
])
}
}
}
</script>
1.2 补丁标记(Patch Flags)
Vue 3 使用补丁标记来标识动态内容,实现精确更新。
vue
<template>
<div>
<!-- TEXT: 1 - 文本内容动态 -->
<span>{{ message }}</span>
<!-- PROPS: 8 - 属性动态 -->
<div :class="className">内容</div>
<!-- CLASS: 2 - class 动态 -->
<div :class="{ active: isActive }">按钮</div>
<!-- STYLE: 4 - style 动态 -->
<div :style="{ color: textColor }">文本</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const message = ref('Hello')
const className = ref('container')
const isActive = ref(false)
const textColor = ref('red')
return {
message,
className,
isActive,
textColor
}
}
}
</script>
1.3 树摇优化(Tree Shaking)
javascript
// 按需导入 Vue 功能
import { ref, computed, watch } from 'vue'
// 而不是导入整个 Vue
// import Vue from 'vue'
// 按需导入组件库
import { ElButton, ElInput } from 'element-plus'
// 而不是全量导入
// import ElementPlus from 'element-plus'
// 使用 unplugin-auto-import 自动导入
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
AutoImport({
imports: ['vue'],
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
}
2. 响应式优化
2.1 合理使用 ref 和 reactive
javascript
// ✅ 推荐:基本类型使用 ref
const count = ref(0)
const message = ref('hello')
const isLoading = ref(false)
// ✅ 推荐:对象使用 reactive
const user = reactive({
id: 1,
name: 'John',
email: 'john@example.com'
})
// ❌ 避免:对象使用 ref(需要 .value 访问)
const userRef = ref({
id: 1,
name: 'John'
})
// 访问时需要 userRef.value.name
// ❌ 避免:基本类型使用 reactive
const state = reactive({
count: 0 // 不如直接用 ref(0)
})
2.2 使用 shallowRef 和 shallowReactive
javascript
// 对于大型对象或数组,使用浅层响应式
const largeList = shallowRef([
{ id: 1, data: /* 大量数据 */ },
{ id: 2, data: /* 大量数据 */ },
// ... 更多数据
])
// 更新整个数组来触发响应式
const addItem = (newItem) => {
largeList.value = [...largeList.value, newItem]
}
// 对于嵌套对象,使用 shallowReactive
const config = shallowReactive({
theme: 'dark',
settings: {
// 这个对象不会是响应式的
notifications: true,
autoSave: false
}
})
// 更新嵌套对象
const updateSettings = (newSettings) => {
config.settings = { ...config.settings, ...newSettings }
}
2.3 使用 markRaw 避免不必要的响应式
javascript
import { markRaw, reactive } from 'vue'
// 标记不需要响应式的对象
const nonReactiveData = markRaw({
heavyComputation: () => { /* 复杂计算 */ },
constants: { PI: 3.14159, E: 2.71828 },
thirdPartyInstance: new SomeLibrary()
})
const state = reactive({
user: { name: 'John', age: 30 },
config: nonReactiveData // 不会被转换为响应式
})
// 对于第三方库实例
const chart = markRaw(new Chart(canvas, options))
const editor = markRaw(new Monaco.Editor())
3. 组件优化
3.1 使用 defineAsyncComponent 异步组件
javascript
// 基本异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // 延迟显示加载组件
timeout: 3000, // 超时时间
suspensible: false,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry() // 重试
} else {
fail() // 失败
}
}
})
// 在路由中使用
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/analytics',
component: () => import('@/views/Analytics.vue')
}
]
3.2 使用 KeepAlive 缓存组件
vue
<template>
<div>
<!-- 缓存所有组件 -->
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
<!-- 选择性缓存 -->
<KeepAlive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 排除特定组件 -->
<KeepAlive :exclude="['ComponentC']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 限制缓存数量 -->
<KeepAlive :max="10">
<component :is="currentComponent" />
</KeepAlive>
</div>
</template>
<script>
// 在组件中处理缓存生命周期
export default {
name: 'CacheableComponent',
activated() {
console.log('组件被激活')
// 刷新数据或重新订阅事件
},
deactivated() {
console.log('组件被缓存')
// 清理定时器或取消订阅
}
}
</script>
3.3 优化组件 props
javascript
// ✅ 使用具体的 prop 类型
export default {
props: {
id: {
type: Number,
required: true
},
user: {
type: Object,
required: true,
validator: (value) => {
return value && typeof value.id === 'number'
}
},
status: {
type: String,
default: 'pending',
validator: (value) => {
return ['pending', 'success', 'error'].includes(value)
}
}
}
}
// ✅ 使用 TypeScript 定义 props
interface Props {
id: number
user: User
status?: 'pending' | 'success' | 'error'
}
const props = defineProps<Props>()
// ✅ 避免传递大型对象,使用计算属性
const userDisplayName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`
})
4. 渲染优化
4.1 使用 v-memo 缓存渲染结果
vue
<template>
<div>
<!-- 缓存列表项渲染 -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id, item.name, item.status]"
>
<h3>{{ item.name }}</h3>
<p>{{ item.description }}</p>
<span :class="item.status">{{ item.status }}</span>
</div>
<!-- 缓存复杂计算结果 -->
<div v-memo="[expensiveValue]">
<ExpensiveComponent :data="expensiveValue" />
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const list = ref([])
const expensiveValue = computed(() => {
// 复杂计算
return list.value.reduce((acc, item) => {
return acc + item.complexCalculation()
}, 0)
})
return { list, expensiveValue }
}
}
</script>
4.2 虚拟滚动优化长列表
vue
<template>
<div class="virtual-list" @scroll="handleScroll">
<div :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{
position: 'absolute',
top: item.top + 'px',
height: itemHeight + 'px'
}"
class="list-item"
>
{{ item.data.name }}
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
props: {
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 50
}
},
setup(props) {
const containerHeight = ref(400)
const scrollTop = ref(0)
const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
const visibleCount = computed(() => {
return Math.ceil(containerHeight.value / props.itemHeight) + 2
})
const startIndex = computed(() => {
return Math.floor(scrollTop.value / props.itemHeight)
})
const visibleItems = computed(() => {
const start = startIndex.value
const end = Math.min(start + visibleCount.value, props.items.length)
return props.items.slice(start, end).map((item, index) => ({
id: item.id,
data: item,
top: (start + index) * props.itemHeight
}))
})
const handleScroll = (event) => {
scrollTop.value = event.target.scrollTop
}
return {
totalHeight,
visibleItems,
handleScroll
}
}
}
</script>
4.3 使用 Suspense 优化异步组件加载
vue
<template>
<div>
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<div class="loading">
<LoadingSpinner />
<p>加载中...</p>
</div>
</template>
</Suspense>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
// 异步组件
const AsyncDashboard = defineAsyncComponent(async () => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
return import('./Dashboard.vue')
})
export default {
components: {
AsyncDashboard,
LoadingSpinner
}
}
</script>
5. 状态管理优化
5.1 使用 Pinia 进行状态管理
javascript
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
currentUser: null,
loading: false
}),
getters: {
// 缓存计算结果
activeUsers: (state) => {
return state.users.filter(user => user.active)
},
// 参数化 getter
getUserById: (state) => {
return (id) => state.users.find(user => user.id === id)
}
},
actions: {
async fetchUsers() {
if (this.loading) return // 防止重复请求
this.loading = true
try {
const users = await api.getUsers()
this.users = users
} finally {
this.loading = false
}
},
// 批量更新
updateUsers(updates) {
this.$patch((state) => {
updates.forEach(update => {
const index = state.users.findIndex(u => u.id === update.id)
if (index !== -1) {
Object.assign(state.users[index], update)
}
})
})
}
}
})
5.2 优化状态订阅
javascript
// 使用 storeToRefs 保持响应性
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
export default {
setup() {
const userStore = useUserStore()
// ✅ 保持响应性
const { users, currentUser, loading } = storeToRefs(userStore)
// ✅ 方法可以直接解构
const { fetchUsers, updateUser } = userStore
// ❌ 避免这样做,会失去响应性
// const { users, currentUser } = userStore
return {
users,
currentUser,
loading,
fetchUsers,
updateUser
}
}
}
6. 网络请求优化
6.1 请求缓存和去重
javascript
// utils/api.js
class ApiClient {
constructor() {
this.cache = new Map()
this.pendingRequests = new Map()
}
async get(url, options = {}) {
const cacheKey = `${url}?${JSON.stringify(options)}`
// 检查缓存
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey)
if (Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存
return cached.data
}
}
// 检查是否有相同的请求正在进行
if (this.pendingRequests.has(cacheKey)) {
return this.pendingRequests.get(cacheKey)
}
// 发起新请求
const request = fetch(url, options)
.then(response => response.json())
.then(data => {
// 缓存结果
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
})
// 清除待处理请求
this.pendingRequests.delete(cacheKey)
return data
})
.catch(error => {
this.pendingRequests.delete(cacheKey)
throw error
})
this.pendingRequests.set(cacheKey, request)
return request
}
}
export const api = new ApiClient()
6.2 使用 SWR 模式
javascript
// composables/useSWR.js
import { ref, watchEffect, onUnmounted } from 'vue'
export function useSWR(key, fetcher, options = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const {
refreshInterval = 0,
revalidateOnFocus = true,
dedupingInterval = 2000
} = options
let intervalId = null
const fetchData = async () => {
if (loading.value) return
loading.value = true
error.value = null
try {
const result = await fetcher(key)
data.value = result
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
// 初始加载
watchEffect(() => {
if (key) {
fetchData()
}
})
// 定期刷新
if (refreshInterval > 0) {
intervalId = setInterval(fetchData, refreshInterval)
}
// 窗口聚焦时重新验证
if (revalidateOnFocus) {
const handleFocus = () => fetchData()
window.addEventListener('focus', handleFocus)
onUnmounted(() => {
window.removeEventListener('focus', handleFocus)
})
}
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId)
}
})
return {
data,
error,
loading,
mutate: fetchData
}
}
// 使用示例
export default {
setup() {
const { data: users, error, loading, mutate } = useSWR(
'/api/users',
(url) => fetch(url).then(res => res.json()),
{
refreshInterval: 30000, // 30秒刷新一次
revalidateOnFocus: true
}
)
return {
users,
error,
loading,
refreshUsers: mutate
}
}
}
7. 构建优化
7.1 代码分割
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
children: [
{
path: 'analytics',
component: () => import('@/views/dashboard/Analytics.vue')
},
{
path: 'reports',
component: () => import('@/views/dashboard/Reports.vue')
}
]
}
]
// 手动代码分割
const AdminModule = () => import('@/modules/admin')
const UserModule = () => import('@/modules/user')
7.2 Vite 构建优化
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
// 将第三方库分离
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
utils: ['lodash', 'dayjs']
}
}
},
// 启用 gzip 压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// 生成 source map
sourcemap: true,
// 设置 chunk 大小警告限制
chunkSizeWarningLimit: 1000
},
// 优化依赖预构建
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia'],
exclude: ['@vueuse/core']
}
})
8. 性能监控
8.1 性能指标收集
javascript
// utils/performance.js
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.observers = []
}
// 监控 Core Web Vitals
observeWebVitals() {
// First Contentful Paint
this.observePerformanceEntry('first-contentful-paint', (entry) => {
this.metrics.FCP = entry.startTime
})
// Largest Contentful Paint
this.observeLCP()
// First Input Delay
this.observeFID()
// Cumulative Layout Shift
this.observeCLS()
}
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.LCP = lastEntry.startTime
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
this.observers.push(observer)
}
observeFID() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach((entry) => {
if (entry.name === 'first-input') {
this.metrics.FID = entry.processingStart - entry.startTime
}
})
})
observer.observe({ entryTypes: ['first-input'] })
this.observers.push(observer)
}
observeCLS() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value
this.metrics.CLS = clsValue
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
this.observers.push(observer)
}
// 监控 Vue 组件性能
measureComponent(name, fn) {
const start = performance.now()
const result = fn()
const end = performance.now()
this.metrics[`component_${name}`] = end - start
return result
}
// 发送性能数据
sendMetrics() {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/metrics', JSON.stringify(this.metrics))
} else {
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(this.metrics),
headers: {
'Content-Type': 'application/json'
}
})
}
}
disconnect() {
this.observers.forEach(observer => observer.disconnect())
}
}
export const performanceMonitor = new PerformanceMonitor()
8.2 Vue DevTools 性能分析
javascript
// 在开发环境中启用性能追踪
if (process.env.NODE_ENV === 'development') {
app.config.performance = true
}
// 自定义性能标记
export default {
setup() {
const heavyComputation = () => {
performance.mark('heavy-computation-start')
// 执行复杂计算
const result = complexCalculation()
performance.mark('heavy-computation-end')
performance.measure(
'heavy-computation',
'heavy-computation-start',
'heavy-computation-end'
)
return result
}
return { heavyComputation }
}
}
9. 最佳实践总结
9.1 开发时优化
javascript
// ✅ 使用 computed 缓存计算结果
const expensiveValue = computed(() => {
return props.list.filter(item => item.active).length
})
// ✅ 使用 watchEffect 自动追踪依赖
watchEffect(() => {
if (props.userId) {
fetchUserData(props.userId)
}
})
// ✅ 合理使用 nextTick
const updateDOM = async () => {
state.value = newValue
await nextTick()
// DOM 已更新
measureElement()
}
// ❌ 避免在模板中使用复杂表达式
// <div>{{ items.filter(i => i.active).map(i => i.name).join(', ') }}</div>
// ✅ 使用计算属性
const activeItemNames = computed(() => {
return items.value
.filter(i => i.active)
.map(i => i.name)
.join(', ')
})
9.2 生产环境优化
javascript
// 环境变量配置
// .env.production
VITE_API_URL=https://api.production.com
VITE_ENABLE_ANALYTICS=true
VITE_LOG_LEVEL=error
// 条件性功能加载
if (import.meta.env.VITE_ENABLE_ANALYTICS === 'true') {
import('./analytics').then(analytics => {
analytics.init()
})
}
// 错误边界
app.config.errorHandler = (err, vm, info) => {
console.error('Vue error:', err, info)
// 发送错误报告
if (import.meta.env.PROD) {
sendErrorReport(err, info)
}
}
总结
Vue 3 性能优化是一个系统性工程,需要从多个维度进行考虑:
- 编译时优化:利用 Vue 3 的编译器优化特性
- 响应式优化:合理使用响应式 API,避免不必要的响应式转换
- 组件优化:使用异步组件、KeepAlive 等优化组件加载和渲染
- 渲染优化:使用 v-memo、虚拟滚动等技术优化大量数据渲染
- 状态管理优化:合理设计状态结构,优化状态更新
- 网络优化:实现请求缓存、去重等策略
- 构建优化:配置代码分割、压缩等构建优化
- 性能监控:建立完善的性能监控体系
通过这些优化策略,可以显著提升 Vue 3 应用的性能和用户体验。