Vue 性能优化深度实战
Vue 性能优化完整指南
1. 组件渲染优化
v-once 与 v-memo
<!-- v-once:只渲染一次,永不更新 -->
<ExpensiveComponent v-once />
<!-- v-memo:依赖的值不变则跳过重渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
{{ item.name }}
</div>
<!-- 组合使用:大列表时非常有效 -->
<RecycleScroller v-memo="[item.id, item.selected]">
<ExpensiveListItem :item="item" />
</RecycleScroller>
计算属性 vs 方法
<script setup>
// computed: 只在依赖变化时重计算,结果被缓存
const sortedList = computed(() =>
[...props.list].sort((a, b) => a.name.localeCompare(b.name))
)
// method: 每次渲染都执行
const getSorted = () => [...props.list].sort(...)
</script>
shallowRef / shallowReactive
// 大列表用 shallowRef 避免深层响应式的开销
const bigList = shallowRef([])
// 更新时需触发更新
bigList.value = [...bigList.value, newItem]
// 威尔用 markRaw 标记不需要响应式的对象
const map = markRaw(new Map())
2. 列表渲染优化
虚拟滚动(vue-virtual-scroller)
<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="60"
key-field="id"
>
<template #default="{ item }">
<ListItem :item="item" />
</template>
</RecycleScroller>
</template>
首屏渲染优化
<!-- 分批渲染大列表,首屏先展示 -->
<script setup>
const visibleCount = ref(50)
const visibleList = computed(() => list.value.slice(0, visibleCount.value))
onMounted(() => {
// 首屏渲染完成后再加载其余
nextTick(() => {
setTimeout(() => {
visibleCount.value = list.value.length
}, 300)
})
})
</script>
3. 路由与分包优化
// 路由懒加载 + 马鼓向加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 分包
children: [
{
path: 'analytics',
component: () => import('./views/Analytics.vue')
}
]
}
]
// 预加载:导航预获取数据
router.beforeResolve(async to => {
if (to.meta.prefetch) {
await prefetchData(to.meta.prefetch)
}
})
分包命名
// webpackChunkName 命名 chunk
const UserProfile = () =>
import(/* webpackChunkName: "user" */ './views/UserProfile.vue')
// Vite 自动分包
// 所有动态导入自动分包
4. 缓存策略
keep-alive 深入
<template>
<!-- 路由级缓存 -->
<router-view v-slot="{ Component, route }">
<transition name="fade">
<keep-alive :include="cachedViews" :max="10">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
// 按需加入缓存列表
const cachedViews = ref(['Dashboard', 'UserList'])
// 切换路由时动态添加
router.afterEach((to) => {
if (to.meta.keepAlive) {
cachedViews.value.push(to.name)
}
})
</script>
keep-alive 的生命周期鋈子
<script setup>
// 组件被激活(从缓存中自展示)
onActivated(() => {
fetchLatestData() // 重新获取数据
})
// 组件被缓存
onDeactivated(() => {
cleanupResources() // 清理定时器、套接字等
})
</script>
5. 网络请求优化
// 并行请求多个接口
const [user, config, permissions] = await Promise.all([
fetchUser(),
fetchConfig(),
fetchPermissions()
])
// 请求去重:将相同请求合并
const pendingRequests = new Map()
function deduplicatedFetch(url: string) {
if (pendingRequests.has(url)) {
return pendingRequests.get(url)
}
const promise = fetch(url).finally(() => {
pendingRequests.delete(url)
})
pendingRequests.set(url, promise)
return promise
}
// 请求预加载:Intersection Observer 视口内就加载
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
prefetchNextPage()
}
})
})
observer.observe(bottomRef.value)
6. 资深面试题
- Vue 3 为什么比 Vue 2 快?
- 编译器优化:静态提升 + PatchFlag + Block Tree
- Proxy 响应式:懒代理,访问时才建立响应式
- Tree-shaking 友好:只包含用到的模块
- v-for 的 key 如何影响性能?
- key 不变:全量更新,运行全量 diff
- key 稳定:精确匹配节点,只更新变化的部分
- key = index:列表尾部操作时没有差异,中间插入错误
- nextTick 的使用场景?
- 修改数据后,需访问更新后的 DOM
- 测如分页切换后的滚动位置重置
- 在 created 中操作 DOM(DOM 还未挂载)
- 如何定位 Vue 应用中的内存泄漏?
- 监听器在 beforeUnmount 中取消
- 定时器、EventBus、WebSocket 在生命周期结束时清理
- Vue DevTools 测量组件数量变化
- 浏览器 Memory Profiler 截图对比