跳到主要内容

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 截图对比