Vue.js 性能优化
为什么需要性能优化?
性能优化可以提升用户体验,减少加载时间,降低服务器负担。本章介绍 Vue 应用的各种性能优化技巧。
1. 代码分割和懒加载
路由懒加载
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('../views/Home.vue') // 懒加载
},
{
path: '/about',
component: () => import('../views/About.vue')
}
]
})组件懒加载
vue
<script setup>
import { defineAsyncComponent } from 'vue'
// 懒加载组件
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>2. 虚拟滚动
对于大列表,使用虚拟滚动只渲染可见区域:
vue
<script setup>
import { ref, computed } from 'vue'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `项目 ${i}`
})))
const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)
const scrollTop = ref(0)
const visibleItems = computed(() => {
const startIndex = Math.floor(scrollTop.value / itemHeight)
const endIndex = startIndex + visibleCount
return items.value.slice(startIndex, endIndex + 1)
})
const offsetY = computed(() => {
return Math.floor(scrollTop.value / itemHeight) * itemHeight
})
function handleScroll(event) {
scrollTop.value = event.target.scrollTop
}
</script>
<template>
<div
class="virtual-scroll-container"
:style="{ height: containerHeight + 'px' }"
@scroll="handleScroll"
>
<div :style="{ height: items.length * itemHeight + 'px', position: 'relative' }">
<div
:style="{ transform: `translateY(${offsetY}px)` }"
class="items-container"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.text }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-scroll-container {
overflow-y: auto;
border: 1px solid #ddd;
}
.item {
display: flex;
align-items: center;
padding: 0 15px;
border-bottom: 1px solid #eee;
}
</style>3. 使用 v-memo 优化渲染
v-memo 可以缓存模板的一部分:
vue
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: '项目 1', selected: false },
{ id: 2, name: '项目 2', selected: false },
{ id: 3, name: '项目 3', selected: false }
])
</script>
<template>
<div v-for="item in items" :key="item.id" v-memo="[item.selected]">
<!-- 只有当 item.selected 变化时才重新渲染 -->
<input type="checkbox" v-model="item.selected" />
<span>{{ item.name }}</span>
</div>
</template>4. 使用 shallowRef 和 shallowReactive
对于大型对象,使用浅层响应式可以提升性能:
vue
<script setup>
import { shallowRef, shallowReactive } from 'vue'
// 只有根级别的属性是响应式的
const state = shallowReactive({
user: {
name: '张三',
profile: {
age: 25,
city: '北京'
}
}
})
// 只有 .value 的变化是响应式的
const data = shallowRef({
items: [/* 大量数据 */]
})
// 更新整个对象
function updateData() {
data.value = { items: [/* 新数据 */] }
}
</script>5. 计算属性缓存
合理使用计算属性,避免重复计算:
vue
<script setup>
import { ref, computed } from 'vue'
const items = ref([/* 大量数据 */])
// ✅ 使用计算属性 - 有缓存
const filteredItems = computed(() => {
console.log('计算 filteredItems')
return items.value.filter(item => item.active)
})
// ❌ 使用方法 - 无缓存
function getFilteredItems() {
console.log('调用 getFilteredItems')
return items.value.filter(item => item.active)
}
</script>
<template>
<div>
<!-- 多次访问只计算一次 -->
<p>{{ filteredItems.length }}</p>
<p>{{ filteredItems.length }}</p>
<!-- 每次访问都会重新计算 -->
<p>{{ getFilteredItems().length }}</p>
<p>{{ getFilteredItems().length }}</p>
</div>
</template>6. 使用 v-once 和 v-memo
v-once - 只渲染一次
vue
<template>
<div v-once>
<!-- 这部分内容只渲染一次,不会更新 -->
<h1>{{ staticTitle }}</h1>
<p>{{ staticContent }}</p>
</div>
</template>v-memo - 条件缓存
vue
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- 只有 item.selected 变化时才重新渲染 -->
{{ item.name }}
</div>
</template>7. 避免不必要的组件更新
使用 v-show 代替 v-if
vue
<template>
<!-- 频繁切换使用 v-show -->
<div v-show="isVisible">内容</div>
<!-- 很少改变使用 v-if -->
<div v-if="isLoggedIn">用户信息</div>
</template>使用 key 优化列表渲染
vue
<template>
<!-- ✅ 使用唯一 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- ❌ 使用索引作为 key(列表会变化时) -->
<div v-for="(item, index) in items" :key="index">
{{ item.name }}
</div>
</template>8. 事件处理优化
使用事件委托
vue
<script setup>
function handleClick(event) {
if (event.target.matches('.item')) {
console.log('点击了项目')
}
}
</script>
<template>
<!-- ✅ 事件委托 - 只绑定一个事件 -->
<div @click="handleClick">
<div class="item">项目 1</div>
<div class="item">项目 2</div>
<div class="item">项目 3</div>
</div>
<!-- ❌ 每个元素都绑定事件 -->
<div>
<div class="item" @click="handleItemClick">项目 1</div>
<div class="item" @click="handleItemClick">项目 2</div>
<div class="item" @click="handleItemClick">项目 3</div>
</div>
</template>防抖和节流
vue
<script setup>
import { ref } from 'vue'
// 防抖
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// 节流
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn.apply(this, args)
}
}
}
const searchQuery = ref('')
// 防抖搜索
const handleSearch = debounce((query) => {
console.log('搜索:', query)
}, 500)
// 节流滚动
const handleScroll = throttle(() => {
console.log('滚动位置:', window.scrollY)
}, 200)
</script>
<template>
<input
v-model="searchQuery"
@input="handleSearch(searchQuery)"
placeholder="搜索..."
/>
</template>9. 图片优化
懒加载图片
vue
<script setup>
import { ref, onMounted } from 'vue'
const images = ref([
'https://picsum.photos/300/200?random=1',
'https://picsum.photos/300/200?random=2',
'https://picsum.photos/300/200?random=3'
])
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img)
})
})
</script>
<template>
<div class="image-gallery">
<img
v-for="(src, index) in images"
:key="index"
:data-src="src"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 200'%3E%3C/svg%3E"
alt="图片"
/>
</div>
</template>使用 WebP 格式
vue
<template>
<picture>
<source srcset="image.webp" type="image/webp" />
<source srcset="image.jpg" type="image/jpeg" />
<img src="image.jpg" alt="图片" />
</picture>
</template>10. 生产环境优化
Vite 配置
javascript
// vite.config.js
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']
}
}
},
// 压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true
}
}
}
})11. 使用 Web Workers
javascript
// worker.js
self.addEventListener('message', (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
})
function heavyComputation(data) {
// 耗时计算
return data * 2
}vue
<script setup>
import { ref } from 'vue'
const result = ref(null)
const worker = new Worker(new URL('./worker.js', import.meta.url))
worker.addEventListener('message', (e) => {
result.value = e.data
})
function startComputation() {
worker.postMessage(1000000)
}
</script>12. 性能监控
vue
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
// 监控首次内容绘制
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FCP:', entry.startTime)
}
})
observer.observe({ entryTypes: ['paint'] })
// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long task:', entry.duration)
}
})
longTaskObserver.observe({ entryTypes: ['longtask'] })
})
</script>性能优化清单
开发阶段
- ✅ 使用计算属性缓存
- ✅ 合理使用 v-if 和 v-show
- ✅ 为列表使用唯一 key
- ✅ 避免在模板中使用复杂表达式
- ✅ 使用事件委托
- ✅ 防抖和节流
构建阶段
- ✅ 代码分割和懒加载
- ✅ Tree Shaking
- ✅ 压缩代码
- ✅ 移除 console
- ✅ 使用生产环境构建
运行时
- ✅ 虚拟滚动
- ✅ 图片懒加载
- ✅ 使用 Web Workers
- ✅ 缓存 API 请求
- ✅ 使用 CDN
总结
- 代码分割和懒加载减少初始加载时间
- 虚拟滚动处理大列表
- 使用 v-memo、v-once 优化渲染
- 计算属性提供缓存
- 防抖节流优化事件处理
- 图片懒加载和 WebP 格式
- 生产环境配置优化
- 性能监控和分析
下一步
接下来学习 测试,了解如何测试 Vue 应用。