Appearance
性能优化
性能优化是构建高质量 Vue.js 应用的重要环节。本章将介绍各种性能优化策略和最佳实践。
渲染性能优化
使用 v-show vs v-if
根据切换频率选择合适的条件渲染指令:
vue
<template>
<!-- 频繁切换使用 v-show -->
<div v-show="isVisible">频繁切换的内容</div>
<!-- 条件很少改变使用 v-if -->
<div v-if="userRole === 'admin'">管理员内容</div>
</template>列表渲染优化
为列表项提供唯一的 key:
vue
<template>
<!-- 不好 -->
<li v-for="item in items">{{ item.name }}</li>
<!-- 好 -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</template>避免在 v-for 中使用 v-if:
vue
<template>
<!-- 不好 -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
<!-- 好 -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
<script>
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
</script>虚拟滚动
对于大量数据的列表,使用虚拟滚动:
vue
<template>
<div class="virtual-list" @scroll="handleScroll" ref="container">
<div :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{
position: 'absolute',
top: item.top + 'px',
height: itemHeight + 'px'
}"
>
{{ item.data }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [], // 所有数据
itemHeight: 50,
containerHeight: 400,
scrollTop: 0
}
},
computed: {
totalHeight() {
return this.items.length * this.itemHeight
},
visibleItems() {
const start = Math.floor(this.scrollTop / this.itemHeight)
const end = start + Math.ceil(this.containerHeight / this.itemHeight)
return this.items.slice(start, end + 1).map((item, index) => ({
id: item.id,
data: item,
top: (start + index) * this.itemHeight
}))
}
},
methods: {
handleScroll(e) {
this.scrollTop = e.target.scrollTop
}
}
}
</script>组件优化
使用异步组件
对于大型组件或不常用的组件,使用异步加载:
javascript
// 异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})组件懒加载
使用 Intersection Observer 实现组件懒加载:
vue
<template>
<div ref="container">
<component :is="shouldLoad ? actualComponent : placeholderComponent" />
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
data() {
return {
shouldLoad: false
}
},
computed: {
actualComponent() {
return defineAsyncComponent(() => import('./HeavyComponent.vue'))
},
placeholderComponent() {
return {
template: '<div class="placeholder">Loading...</div>'
}
}
},
mounted() {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.shouldLoad = true
observer.disconnect()
}
})
observer.observe(this.$refs.container)
}
}
</script>使用 KeepAlive
缓存不活跃的组件实例:
vue
<template>
<KeepAlive :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</KeepAlive>
</template>动态控制缓存:
vue
<template>
<KeepAlive :max="10">
<router-view v-if="$route.meta.keepAlive" />
</KeepAlive>
<router-view v-if="!$route.meta.keepAlive" />
</template>响应式优化
使用 shallowRef 和 shallowReactive
对于大型对象,使用浅层响应式:
javascript
import { shallowRef, shallowReactive } from 'vue'
// 只有 .value 的访问会被追踪
const state = shallowRef({ count: 1 })
// 只有根级别的属性是响应式的
const state2 = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})使用 markRaw
标记对象为非响应式:
javascript
import { markRaw } from 'vue'
const foo = markRaw({
nested: {}
})
const bar = reactive({
// 虽然 `foo` 被标记为了 raw,但 foo.nested 不是。
nested: foo.nested
})避免不必要的响应式
javascript
// 不好 - 不需要响应式的数据
data() {
return {
expensiveData: this.processLargeDataset(),
staticConfig: { /* 大量静态配置 */ }
}
}
// 好 - 将静态数据移到组件外部
const STATIC_CONFIG = { /* 大量静态配置 */ }
export default {
data() {
return {
expensiveData: null
}
},
created() {
// 在需要时才处理数据
this.expensiveData = this.processLargeDataset()
}
}计算属性优化
避免在计算属性中进行副作用操作
javascript
// 不好
computed: {
expensiveValue() {
// 副作用操作
this.doSomethingExpensive()
return this.someValue * 2
}
}
// 好
computed: {
expensiveValue() {
return this.someValue * 2
}
},
watch: {
someValue() {
this.doSomethingExpensive()
}
}使用计算属性缓存
javascript
// 利用计算属性的缓存特性
computed: {
filteredItems() {
// 只有当 items 或 filter 改变时才重新计算
return this.items.filter(item =>
item.name.includes(this.filter)
)
}
}事件处理优化
事件委托
对于大量相似元素的事件处理,使用事件委托:
vue
<template>
<div @click="handleClick">
<button data-action="save">保存</button>
<button data-action="cancel">取消</button>
<button data-action="delete">删除</button>
</div>
</template>
<script>
export default {
methods: {
handleClick(event) {
const action = event.target.dataset.action
if (action) {
this[`handle${action.charAt(0).toUpperCase() + action.slice(1)}`]()
}
},
handleSave() { /* ... */ },
handleCancel() { /* ... */ },
handleDelete() { /* ... */ }
}
}
</script>防抖和节流
对于频繁触发的事件,使用防抖或节流:
javascript
import { debounce, throttle } from 'lodash-es'
export default {
methods: {
// 防抖 - 搜索输入
handleSearch: debounce(function(query) {
this.search(query)
}, 300),
// 节流 - 滚动事件
handleScroll: throttle(function(event) {
this.updateScrollPosition(event)
}, 100)
}
}内存优化
清理定时器和事件监听器
javascript
export default {
data() {
return {
timer: null
}
},
mounted() {
this.timer = setInterval(() => {
// 定时任务
}, 1000)
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
// 清理定时器
if (this.timer) {
clearInterval(this.timer)
}
// 清理事件监听器
window.removeEventListener('resize', this.handleResize)
}
}避免内存泄漏
javascript
// 避免在组件中保存 DOM 引用
export default {
data() {
return {
// 不好
// domElement: null
}
},
mounted() {
// 不好 - 保存 DOM 引用可能导致内存泄漏
// this.domElement = document.querySelector('.some-element')
// 好 - 直接使用,不保存引用
const element = document.querySelector('.some-element')
element.addEventListener('click', this.handleClick)
}
}网络优化
请求合并
合并多个 API 请求:
javascript
// 不好 - 多个独立请求
async loadUserData() {
const profile = await api.getUserProfile()
const settings = await api.getUserSettings()
const notifications = await api.getNotifications()
return { profile, settings, notifications }
}
// 好 - 合并请求
async loadUserData() {
const [profile, settings, notifications] = await Promise.all([
api.getUserProfile(),
api.getUserSettings(),
api.getNotifications()
])
return { profile, settings, notifications }
}请求缓存
实现简单的请求缓存:
javascript
class ApiCache {
constructor() {
this.cache = new Map()
this.ttl = 5 * 60 * 1000 // 5分钟
}
get(key) {
const item = this.cache.get(key)
if (item && Date.now() - item.timestamp < this.ttl) {
return item.data
}
return null
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
}
const apiCache = new ApiCache()
export default {
methods: {
async fetchUserData(userId) {
const cacheKey = `user-${userId}`
let userData = apiCache.get(cacheKey)
if (!userData) {
userData = await api.getUser(userId)
apiCache.set(cacheKey, userData)
}
return userData
}
}
}构建优化
代码分割
使用动态导入进行代码分割:
javascript
// 路由级别的代码分割
const routes = [
{
path: '/home',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
]
// 组件级别的代码分割
export default {
components: {
HeavyComponent: () => import('./HeavyComponent.vue')
}
}Tree Shaking
确保只导入需要的模块:
javascript
// 不好 - 导入整个库
import _ from 'lodash'
// 好 - 只导入需要的函数
import { debounce, throttle } from 'lodash-es'
// 或者使用具体的模块
import debounce from 'lodash-es/debounce'预加载关键资源
vue
<template>
<div>
<!-- 预加载关键图片 -->
<link rel="preload" as="image" href="/critical-image.jpg">
<!-- 预连接到外部域名 -->
<link rel="preconnect" href="https://api.example.com">
</div>
</template>性能监控
使用 Performance API
javascript
// 测量组件渲染时间
export default {
beforeMount() {
performance.mark('component-mount-start')
},
mounted() {
performance.mark('component-mount-end')
performance.measure(
'component-mount',
'component-mount-start',
'component-mount-end'
)
const measure = performance.getEntriesByName('component-mount')[0]
console.log(`组件挂载耗时: ${measure.duration}ms`)
}
}长任务监控
javascript
// 监控长任务
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn(`长任务检测: ${entry.duration}ms`)
}
}
})
observer.observe({ entryTypes: ['longtask'] })
}性能检查清单
- [ ] 使用生产环境构建
- [ ] 启用 gzip 压缩
- [ ] 使用 CDN 分发静态资源
- [ ] 实现代码分割和懒加载
- [ ] 优化图片(WebP 格式、适当尺寸)
- [ ] 使用 Service Worker 缓存
- [ ] 减少 HTTP 请求数量
- [ ] 避免不必要的重新渲染
- [ ] 使用虚拟滚动处理大列表
- [ ] 实现骨架屏提升感知性能