Skip to content

Vue 性能优化

大白话解释: Vue 性能优化就像"让车跑得更快"。有几种方式:

  • 减少渲染次数:不该更新的组件不要更新
  • 减少渲染内容:只渲染用户看到的部分
  • 缓存组件:切换页面时不销毁,保留状态
  • 减小打包体积:只打包用到的代码

为什么要性能优化?

  • 用户体验:页面加载慢、操作卡顿,用户会流失
  • SEO 排名:加载速度影响搜索引擎排名
  • 资源消耗:减少服务器和用户设备的负担

四个层面的优化:

  • 组件层面:v-once、v-memo、shallowRef、KeepAlive
  • 渲染层面:虚拟滚动、懒加载、按需引入
  • 状态层面:合理使用 computed、避免不必要的响应式
  • 构建层面:代码分割、Tree Shaking、CDN

Vue 3 项目中常用的性能优化手段,从组件、渲染、状态、构建四个层面总结。


一、组件层面

v-once —— 静态内容缓存

vue
<!-- 渲染一次后不再更新,Vue 会跳过该节点的 diff -->
<p v-once>这段文字永远不会变:{{ staticText }}</p>

<!-- 整个组件树只渲染一次 -->
<div v-once>
  <h1>{{ title }}</h1>
  <p>{{ description }}</p>
</div>

💡 适合纯静态内容,如文章正文、协议条款等。

v-memo —— 条件缓存(Vue 3.2+)

vue
<template>
  <!-- 只有 item.id === selected 或 item.updated 变化时才重新渲染 -->
  <div
    v-for="item in list"
    :key="item.id"
    v-memo="[item.id === selected, item.updated]"
  >
    <p>id: {{ item.id }}</p>
    <p>是否选中: {{ item.id === selected }}</p>
    <p>更新时间: {{ item.updated }}</p>
  </div>
</template>

💡 大列表中性能提升显著。v-memo 数组中的值都不变时,整个 vnode 会被跳过。

shallowRef / shallowReactive

ts
import { shallowRef, shallowReactive } from 'vue'

// 只有 .value 整体替换才触发更新
const hugeArray = Array.from({ length: 10000 }, (_, i) => ({ id: i, data: `item-${i}` }))
const bigData = shallowRef(hugeArray)

// 只有第一层是响应式的
const state = shallowReactive({
  name: '张三',       // ✅ 响应式
  nested: { count: 0 }, // ❌ 不是响应式的
})

💡 适合大型数据结构只关心引用变化的场景,如从接口返回的列表。

defineAsyncComponent —— 异步组件

ts
import { defineAsyncComponent } from 'vue'

// 基本懒加载
const HeavyComponent = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// 带选项
const AdminPanel = defineAsyncComponent({
  loader: () => import('./AdminPanel.vue'),
  delay: 200,              // 延迟显示 loading(避免闪烁)
  timeout: 10000,          // 超时时间
  errorComponent: ErrorDisplay,
  loadingComponent: LoadingSpinner,
  // 失败重试
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) retry()
    else fail()
  },
})

KeepAlive —— 缓存组件

vue
<template>
  <KeepAlive :max="10" include="PageA,PageB" exclude="PageC">
    <component :is="currentComponent" />
  </KeepAlive>
</template>
vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

// 缓存激活时
onActivated(() => {
  console.log('组件被激活')
  // 可重新获取数据
})

// 进入缓存时
onDeactivated(() => {
  console.log('组件被缓存')
  // 清理副作用
})
</script>

⚠️ max 设置最大缓存数,避免内存膨胀。

组件拆分原则

vue
<!-- ❌ 单文件 2000 行,每次数据变化整个组件重新渲染 -->
<template>
  <Header />
  <Sidebar />
  <Table :data="hugeList" />
  <Chart :data="hugeList" />
  <Footer />
</template>

<!-- ✅ 拆分为独立组件,只有数据变化的组件重新渲染 -->
<template>
  <Header />
  <Sidebar />
  <DataTable :data="hugeList" />    <!-- 只有 list 变化时重新渲染 -->
  <DataChart :data="chartData" />   <!-- 只有 chartData 变化时重新渲染 -->
  <Footer />                        <!-- 永远不重新渲染 -->
</template>

二、渲染层面

虚拟列表

长列表使用虚拟滚动,只渲染可见区域,避免大量 DOM 节点拖慢性能。

bash
# 推荐库:@tanstack/vue-virtual
yarn add @tanstack/vue-virtual
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'

// 父容器 ref
const parentRef = ref<HTMLElement>()

// 模拟 10000 条数据
const items = Array.from({ length: 10000 }, (_, i) => `第 ${i} 条`)

// 创建虚拟化实例 —— 只渲染可见区域的行
const virtualizer = useVirtualizer({
  count: items.length,             // 总条数
  getScrollElement: () => parentRef.value!,  // 滚动容器
  estimateSize: () => 35,          // 每行预估高度(px)
  overscan: 5,                     // 可见区域外多渲染的行数
})
</script>

<template>
  <!-- 外层容器:固定高度 + overflow 自动滚动 -->
  <div ref="parentRef" style="height: 400px; overflow: auto;">
    <!-- 内层撑开总高度,让滚动条正确 -->
    <div :style="{ height: virtualizer.getTotalSize() + 'px', position: 'relative' }">
      <!-- 只渲染可见行 -->
      <div
        v-for="row in virtualizer.getVirtualItems()"
        :key="row.key"
        :style="{ position: 'absolute', top: row.start + 'px', width: '100%' }"
      >
        {{ items[row.index] }}
      </div>
    </div>
  </div>
</template>

💡 详细用法见 TanStack Virtual 文档

v-if vs v-show

指令初始渲染切换开销适用场景
v-if懒加载,条件为 false 不渲染高(销毁/重建)条件很少改变
v-show始终渲染低(CSS 切换)频繁切换

事件优化(防抖 / 节流)

vue
<script setup lang="ts">
import { useThrottleFn, useDebounceFn } from '@vueuse/core'

// 滚动节流 —— 每 200ms 最多执行一次
const onScroll = useThrottleFn(() => { /* ... */ }, 200)

// 搜索防抖 —— 停止输入 300ms 后执行
const onSearch = useDebounceFn((keyword: string) => { /* ... */ }, 300)
</script>

避免不必要的渲染

vue
<script setup lang="ts">
import { computed } from 'vue'

// ❌ 每次渲染都创建新数组
// <div v-for="item in list.filter(i => i.active)">

// ✅ 用 computed 缓存
const activeItems = computed(() => list.value.filter((i) => i.active))
</script>

<template>
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

三、状态层面

精确更新,避免大对象替换

ts
// ❌ 整个对象替换,触发所有依赖该对象的 watcher
state.user = { ...state.user, name: '新名字' }

// ✅ 只修改需要的属性
state.user.name = '新名字'

computed 缓存

ts
// 假设 bigList 是一个 Ref<Array<{ name: string; active: boolean }>>
const bigList = ref(Array.from({ length: 10000 }, (_, i) => ({ name: `item-${i}`, active: i % 2 === 0 })))

// ❌ 每次访问都重新计算
function expensive() {
  return bigList.value.filter(...).map(...).reduce(...)
}

// ✅ computed 有缓存,只在依赖变化时重新计算
const result = computed(() => {
  return bigList.value.filter(...).map(...).reduce(...)
})

避免深度监听

ts
// 假设 obj 的类型为 Ref<{ name: string; age: number }>
const obj = ref({ name: '张三', age: 25 })

// ❌ deep: true 性能差,会递归遍历对象所有层级
watch(obj, handler, { deep: true })

// ✅ 精确监听某个属性,只在 name 变化时触发
watch(() => obj.value.name, handler)

四、构建层面

Vite 构建配置详见 Vue 3 + Vite 开发指南

路由懒加载

ts
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'), // 自动 code splitting
  },
]

组件库按需引入

ts
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

export default {
  plugins: [
    Components({
      resolvers: [NaiveUiResolver()],
    }),
  ],
}

分包策略

ts
// vite.config.ts — 将大依赖拆为独立 chunk
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'echarts': ['echarts'],
        },
      },
    },
  },
})

📖 图片优化、Gzip 压缩、CDN 加速等通用构建优化见 前端性能优化


五、Bundle 分析

bash
# 安装分析工具
yarn add -D rollup-plugin-visualizer
ts
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default {
  plugins: [
    visualizer({
      open: true,              // 构建后自动打开报告
      filename: 'stats.html',  // 输出文件名
      gzipSize: true,          // 显示 gzip 后大小
    }),
  ],
}
bash
# 构建并查看分析报告
yarn build
# 自动打开 stats.html,查看各模块体积占比

💡 找出体积最大的模块,考虑按需引入、替换轻量库、或拆分为异步 chunk。


六、开发调试

ts
// 使用 onRenderTracked / onRenderTriggered 调试响应式
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  console.log('tracked:', event.target, event.key)
})

onRenderTriggered((event) => {
  console.log('triggered:', event.target, event.key, event.oldValue, event.newValue)
})
ts
// Vue Devtools 性能分析
// 打开 Vue Devtools → Performance → Start recording

七、性能优化清单

类别优化手段效果
组件路由懒加载减少首屏 JS 体积
组件异步组件 defineAsyncComponent按需加载重型组件
组件KeepAlive 缓存避免重复渲染
渲染虚拟列表大列表流畅滚动
渲染v-memo / v-once跳过静态节点 diff
渲染事件节流/防抖减少高频触发
状态computed 缓存避免重复计算
状态shallowRef减少深层代理开销
构建分包策略并行加载,缓存复用
构建图片 WebP + 懒加载减少资源体积
构建Gzip/Brotli 压缩传输体积减小 60%+(需服务器配置)
分析rollup-plugin-visualizer定位体积瓶颈

💡 Gzip/Brotli 压缩需要在 Web 服务器(如 Nginx)或 CDN 层配置,前端构建工具(如 vite-plugin-compression)仅负责生成 .gz/.br 预压缩文件。KeepAlive 的 include/exclude 除逗号分隔字符串外,还支持 RegExp 类型。


参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准