Vue 项目性能优化

在 Vue 项目中进行性能优化,需要从构建体积、渲染效率、网络加载、内存管理等多个维度系统性地入手。下面按模块拆解常见优化方案,每个方案都配代码实例、适用场景、注意事项及真实踩坑记录。


一、路由与组件懒加载(代码分割)

方案说明

将不同路由对应的组件拆分成独立 chunk,仅在访问时才加载,减小首屏 JS 体积。

代码示例

javascript 复制代码
// router/index.js
const Home = () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
const About = () => import(/* webpackChunkName: "about" */ '@/views/About.vue')

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

使用场景

  • SPA 中所有路由级别页面。
  • 大型组件(如富文本编辑器、图表库)也应用动态引入。

注意事项

  • 使用命名 chunk(webpackChunkName)方便定位缓存问题。
  • 若组件加载失败,应有错误边界处理(errorCaptured 或 <Suspense>)。
  • 避免拆分过细导致 HTTP 请求过多(通常按路由划分即可)。

遇到的坑

坑: 未配置 publicPath 或部署路径错误,动态 chunk 会 404。

解决: 确认 vue.config.jspublicPath 与实际部署目录一致。

坑: 低版本浏览器不支持 import(),需要 Babel 插件 @babel/plugin-syntax-dynamic-import


二、合理使用 v-ifv-show,及 keep-alive 缓存

方案说明

  • v-if:条件性渲染,切换开销大,适合运行时条件很少改变的场景。
  • v-show:初始渲染开销大,但切换仅改 CSS display,适合频繁切换。
  • <keep-alive>:缓存不活动的组件实例,避免重复渲染。

代码示例

html 复制代码
<!-- 频繁切换的 Tab 使用 v-show 并配合 keep-alive -->
<keep-alive>
  <component :is="currentTab" v-show="activeTab === 'tab1'"></component>
</keep-alive>

使用场景

  • 表单多步骤向导(keep-alive 保留已填数据)。
  • Tab 切换内容不丢失状态。

注意事项

  • keep-alive 会占用内存,避免缓存过多大型组件,可配合 max 属性。
  • 使用 activated/deactivated 生命周期代替 mounted/destroyed 处理数据刷新。

遇到的坑

坑: 使用 keep-alive 但列表页数据不刷新,因为 created 不再执行。

解决:activated 钩子中请求数据,并设置条件判断是否需要刷新。

坑: keep-alive 包裹的组件过多导致内存泄漏,某些第三方库事件监听未及时清理。

解决:deactivated 中手动解绑,或使用 beforeRouteLeave 做清理。


三、列表渲染优化(虚拟滚动)

方案说明

当需要渲染成千上万条数据时,全量 DOM 节点会严重卡顿。使用虚拟滚动库(如 vue-virtual-scroller)仅渲染可视区域内的节点。

代码示例

html 复制代码
<template>
  <RecycleScroller
    class="scroller"
    :items="list"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">{{ item.name }}</div>
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>

使用场景

  • 聊天记录、日志列表、无限滚动商品列表。
  • 长表格、大型树结构。

注意事项

  • 项目需稳定高度或可使用 dynamic 模式,但性能稍差。
  • 虚拟滚动内不宜使用复杂的交互或过渡动画,易引起计算错误。

遇到的坑

坑: 动态高度元素(如图片)在虚拟滚动中错位或闪烁。

解决: 使用 DynamicScroller 并配合 DynamicScrollerItem,确保图片加载后更新高度。

坑: 虚拟滚动库可能不兼容某些 Vue Devtools 检查,调试困难。

解决: 开发时临时关闭虚拟滚动(用条件渲染),或单独抽离列表组件。


四、计算属性与 v-memo 减少重复计算/渲染

方案说明

  • 利用 computed 缓存派生数据,避免模板中直接编写复杂逻辑。
  • Vue3 的 v-memo 可以缓存子树,当依赖不变时跳过虚拟 DOM 生成。

代码示例

html 复制代码
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <span>{{ expensiveFormat(item) }}</span>
  </div>
</template>

使用场景

  • 大列表中某个状态(如选中)频繁切换,其他项无需重新渲染。
  • 重复使用昂贵计算结果的地方。

注意事项

  • v-memo 依赖数组必须明确,否则可能导致不更新。
  • 过度使用 v-memo 可能产生隐藏 bug,建议仅在性能瓶颈处使用。

遇到的坑

坑: v-memo 用于带有 v-for 的组件时,如果内部嵌套了 slot 且 slot 数据变化,但依赖项不变,可能导致视图不更新。

解决: 仔细分析依赖,将真正影响子树输出的响应式数据都加入依赖数组。


五、避免 v-ifv-for 同级使用

方案说明

Vue 会将 v-if 优先级置于 v-for 之上(Vue3 中 v-if 优先级更高),可能导致遍历前就判断条件,逻辑错误且性能低下。应使用计算属性预先过滤列表。

代码示例

html 复制代码
<!-- 错误 -->
<div v-for="user in users" v-if="user.active" :key="user.id">

<!-- 正确 -->
<template v-for="user in activeUsers" :key="user.id">
  <div>{{ user.name }}</div>
</template>

<script setup>
import { computed } from 'vue'
const activeUsers = computed(() => users.value.filter(u => u.active))
</script>

场景 & 注意

  • 任何需要过滤列表再渲染的情况。
  • 过滤逻辑应放在计算属性中以保持缓存。

坑点

坑: 直接在 v-for 里面嵌套计算属性调用函数,仍然会造成每次渲染都执行过滤。

解决: 务必使用 computed 生成新数组。


六、图片与资源优化

1. 图片懒加载(Intersection Observer)

html 复制代码
<img v-lazy="imageSrc" />

使用 vue-lazyload 插件,或自定义指令。

2. 响应式图片(WebP + srcset)

html 复制代码
<picture>
  <source srcset="img.webp" type="image/webp" />
  <img src="img.jpg" loading="lazy" />
</picture>

3. 精灵图/ SVG 图标雪碧图

减少 HTTP 请求,使用 svg-sprite-loader

使用场景

  • 电商列表页、图片密集页面。

注意事项

  • 懒加载需设置占位图防止布局抖动。
  • 动态 WebP 生成服务(如 CDN 转换)需考虑回退。

坑: 懒加载指令在元素 display:nonev-if 重新显示时可能不会触发检测。

解决: 在激活时手动调用指令的 update 或重新绑定。


七、第三方库按需引入与 Tree Shaking

方案说明

确保使用支持 Tree Shaking 的库,并按需导入,避免引入整个库。

代码示例

javascript 复制代码
// 错误:导入整个 lodash
import _ from 'lodash'

// 正确:按需导入
import debounce from 'lodash/debounce'

// 针对 Element Plus 使用 unplugin-vue-components 自动按需
import { ElButton } from 'element-plus'

配置 Babel/Webpack/Vite

  • vite-plugin-components (Vite) 或 babel-plugin-import (Webpack) 实现组件按需加载。

坑: 某些库如 moment.js 无法 Tree Shaking,且 locale 文件大。

解决: 迁移到 day.jsdate-fns,并使用 IgnorePlugin 排除多余语言包。


八、Webpack/Vite 构建层优化

1. 压缩与去除注释

javascript 复制代码
// vite.config.js
build: {
  minify: 'terser',
  terserOptions: {
    compress: { drop_console: true, drop_debugger: true }
  }
}

2. 拆包策略(Vendor Splitting)

Vite 基于 Rollup:

javascript 复制代码
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['vue', 'vue-router', 'pinia'],
        ui: ['element-plus']
      }
    }
  }
}

Webpack 使用 splitChunks

3. 压缩图片与资源(插件)

vite-plugin-imagemin

4. Gzip/Brotli 压缩

构建生成 .gz 文件,由 Nginx 直接提供静态预压缩资源。

使用场景

  • 所有生产构建项目。

坑: 拆包过细导致 HTTP/2 并发请求过多,反而变慢。

解决: 根据业务合并,通常 vendor + 公共工具 + 业务页面三类即可。

坑: drop_console 可能在有意的日志关键逻辑中误删。

解决: 保留 console.warn/error,或在代码中使用自定义 logger 并用环境变量控制。


九、事件销毁与内存泄漏防范

方案说明

组件销毁时必须清除定时器、全局事件绑定、Observer 等,防止内存泄漏和后台持续执行。

代码示例

javascript 复制代码
export default {
  mounted() {
    this.scrollHandler = () => { /* heavy task */ }
    window.addEventListener('scroll', this.scrollHandler)
    this.timer = setInterval(this.refresh, 5000)
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.scrollHandler)
    clearInterval(this.timer)
  }
}

Vue3 可使用 onBeforeUnmount

场景

  • 使用全局事件总线(mitt)、WebSocket、ECharts 实例、setInterval。

坑: 使用了 keep-alive 的组件,beforeDestroy 不会触发,需用 deactivated 清理临时资源。

坑: 引入第三方图表库时,实例未销毁导致切换路由后 DOM 不存在,控制台报错且内存增加。


十、预加载与预取策略

方案说明

利用 <link rel="prefetch/preload"> 加速后续页面或关键资源加载。

代码示例

手动在 index.html 添加:

html 复制代码
<link rel="prefetch" href="/css/chunk-vendors.css" as="style" />

Webpack 可在魔法注释中实现:

javascript 复制代码
const Profile = () => import(/* webpackPrefetch: true */ '@/views/Profile')

使用场景

  • 预估用户下一步操作(如登录后一定去首页),提前加载首屏下页资源。
  • 关键字体、CSS 使用 preload。

注意事项

  • prefetch 会占用带宽,移动端慎用。
  • preload 不宣载过多资源,否则阻塞主文档。

坑: webpackPrefetch 在某些路由权限判断下,未登录用户会预取需认证的页面,白白浪费流量。

解决: 在路由守卫中判断权限后再动态插入 link 标签。


十一、服务端渲染(SSR)与静态生成(SSG)

方案说明

使用 Nuxt.js 或 Vite SSR 将首屏 HTML 直接服务端渲染,降低白屏时间,利于 SEO。

场景

  • 内容型网站、电商详情页、博客。
  • 对首屏速度有极致要求的项目。

注意事项

  • 需要 Node 服务器部署,增加运维成本。
  • 注意第三方库是否支持服务端(如 window 访问受限),需做环境判断。
  • 避免内存泄漏:服务端请求上下文需独立,不能使用单例状态。

坑: 未正确处理客户端激活(hydration),导致 DOM 不匹配错误。

解决: 确保服务端与客户端渲染的数据一致,使用 <ClientOnly> 包裹纯客户端组件。


十二、合理使用 shallowRefshallowReactive

方案说明

当数据结构非常大且嵌套深,但只需表层响应式时,用浅响应式 API 避免深度遍历代理,减少初始化开销和内存占用。

代码示例

javascript 复制代码
import { shallowRef } from 'vue'
const largeList = shallowRef(new Array(10000).fill().map((_,i) => ({ id: i })))
// 只有当替换整个数组引用时才触发更新
function update() {
  largeList.value = [...largeList.value, newItem] 
}

场景

  • 持有巨型数据的变量,但视图只依赖顶层引用变化。
  • 与不可变数据模式结合使用。

坑: 不小心修改内部属性不会触发更新,调试困难。团队需严格遵守数据不可变规范。


总结:性能优化需遵循"测量 → 定位瓶颈 → 实施 → 再测量"的闭环。不应提前过度优化,优先解决网络、JS 体积、渲染阻塞等影响首屏体验的明显问题。基于 Lighthouse 和 Vue Devtools 性能面板持续监控。

相关推荐
辞忧九千七1 小时前
Vue3 学习:组件通信完全指南
vue.js
YJlio1 小时前
OpenClaw 2026.5.2 Beta 更新解读:外部插件安装、ClawHub / npm 切换与 Gateway 性能优化
性能优化·npm·gateway·飞书·多维表格·飞书aily·飞书妙搭
魔术师Grace1 小时前
真正值钱的 AI 小工具,可能只是帮人少打一遍字
前端·人工智能
YanDDDeat1 小时前
MySQL性能排查,慢查询导致CPU飙高的完整记录
mysql·安全·性能优化
用户新4 小时前
JS事件深度解析四 事件的循环和异步
前端·javascript·事件·event loop
广州灵眸科技有限公司10 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) Easy-Eai编译环境准备与更新
服务器·前端·人工智能·python·深度学习
万少11 小时前
我把 Kimi 接进微信,几分钟做了个随手出图助手
前端
xiaofeichaichai11 小时前
网络请求与实时通道
前端·网络