哈喽,各位奋斗在 Vue 3 一线的大佬和小伙伴们!咱们用 Vue 3 写代码,是不是感觉 Composition API 贼爽,<script setup>
贼方便?但爽归爽,项目一大,用户一多,性能问题可能还是会悄悄找上门来。卡顿、白屏、加载慢... 别慌!
今天这篇,咱们不讲虚的,就用大白话聊聊怎么给你的 Vue 3 项目"对症下药",做性能优化。同时,也扒一扒咱们常用的构建工具 Webpack 和新晋网红 Vite,看看怎么让它们也跑得更快!
为啥要做性能优化?(老生常谈但重要)
- 用户体验:快=爽!慢=烦!没人喜欢等。
- 留存率:应用流畅,用户才爱用,才不卸载。
- 业务目标:性能可能直接影响转化率、用户满意度。
- 开发体验:构建快了,调试爽了,咱码农也能少点焦虑不是?😉
Part 1:Vue 3 专属性能优化技巧 ✨
Vue 3 底层做了很多优化(比如基于 Proxy 的响应式系统、更强的 Tree-shaking),但咱们还能锦上添花!
1. 理解 Composition API 的优势 (间接优化)
虽然不是直接的性能命令,但 Composition API 能帮你更好地组织代码:
- 逻辑复用 :把相关逻辑抽到
useXXX()
函数里,组件更干净,也更容易维护。 - 更好的 Tree-shaking:相比 Options API,按需导入的函数更容易被构建工具"摇掉"没用到的代码,减小包体积。
大白话:就像你收拾屋子,把同类的东西放一起(逻辑组织),不用的东西扔掉(Tree-shaking),屋子就显得更大更整洁了。
2. v-memo
:给你的模板片段加个"缓存锁" 🔒
场景:你的模板里有一大块内容,它依赖的数据很少变,但它所在的组件却因为其他数据变化而频繁重新渲染。
问题:每次父组件更新,这块"稳定"的内容也跟着重新计算和渲染,浪费性能。
Vue 3 的解药 :用 v-memo
!
大白话 :v-memo
就像给这块模板加了个"记忆"。你告诉它:"嘿,你就看这几个数据(v-memo
数组里的依赖),只要它们不变,你就别重新渲染了,用上次的结果就行!"
代码示例:
javascript
<template>
<div>
<!-- 这个 div 依赖 list,只有 list 变化时才重新渲染 -->
<div v-memo="[list]">
<p>列表项数量: {{ list.length }}</p>
<ul>
<!-- 假设这个循环非常耗性能 -->
<li v-for="item in list" :key="item.id" class="heavy-item">
{{ item.name }} - {{ complexCalculation(item.value) }}
</li>
</ul>
</div>
<!-- 这个按钮只改变 unrelatedData,不会导致上面 v-memo 的 div 重新渲染 -->
<button @click="unrelatedData++">更新无关数据</button>
<p>无关数据: {{ unrelatedData }}</p>
<!-- 这个按钮改变 list,会触发 v-memo 的 div 重新渲染 -->
<button @click="addItem">添加列表项</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const list = ref([
{ id: 1, name: '苹果', value: 10 },
{ id: 2, name: '香蕉', value: 5 },
]);
const unrelatedData = ref(0);
// 假设这是一个非常耗时的计算
function complexCalculation(value) {
console.log('复杂计算执行了 for value:', value);
// 模拟耗时
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(i * value);
}
return result.toFixed(2);
}
function addItem() {
list.value.push({ id: Date.now(), name: '新水果', value: Math.random() * 10 });
}
</script>
<style>
.heavy-item {
padding: 5px;
border-bottom: 1px solid #eee;
}
</style>
注意:v-memo="[]" 表示永远不更新(除非组件重新创建)。v-memo 不能用在 v-for 内部的元素上,要用在 v-for 的父元素或者包含 v-for 的块上。
3. v-once:一次性渲染,永不更新 🗿
这个 Vue 2 就有,Vue 3 依然好用。
场景:有些内容从显示出来那一刻起,就再也不会变了。
大白话:告诉 Vue:"这块内容,你渲染一次就行了,以后别管它了,我保证它不变。"
代码示例:
javascript
<template>
<div>
<!-- 这段版权信息永远不变 -->
<footer v-once>
Copyright © {{ currentYear }} My Awesome App. All rights reserved.
</footer>
<!-- currentYear 即使在 setup 中是响应式 ref,这里也只会用初始值渲染一次 -->
</div>
</template>
<script setup>
import { ref } from 'vue';
// 假设 currentYear 是动态获取的,但 footer 只需要渲染时那一刻的值
const currentYear = ref(new Date().getFullYear());
</script>
4. 虚拟列表/滚动:处理海量数据不卡顿 📜
场景:你需要显示一个超级长的列表(成千上万条数据)。
问题:一次性渲染所有列表项到 DOM,浏览器会卡死,内存爆炸。
解决方案:虚拟列表(Virtual Scrolling)。
大白话:别傻乎乎地把一万本书都搬到桌子上,桌子会塌!你只需要把用户当前能看到的几本书(以及上下滚动时即将看到的几本)放到桌子上就行了。用户滚动时,动态地替换桌子上的书。
实现:Vue 3 没有内置,但有很多成熟的库可以用:
- vue-virtual-scroller
- @tanstack/vue-virtual (来自 TanStack Query/Table 的作者)
代码示例 (概念,具体看库文档) :
xml
<template>
<!-- 使用虚拟滚动组件 -->
<RecycleScroller
class="scroller"
:items="hugeList"
:item-size="32" v-slot="{ item }">
<div class="user">
{{ item.name }}
</div>
</RecycleScroller>
</template>
<script setup>
import { ref } from 'vue';
// import { RecycleScroller } from 'vue-virtual-scroller'; // 假设已安装并导入
// import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
const hugeList = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `User ${i}` })));
</script>
<style>
.scroller {
height: 400px; /* 必须给容器一个固定的高度 */
overflow-y: auto;
}
.user {
height: 32px; /* 需要知道每个列表项的高度 */
line-height: 32px;
border-bottom: 1px solid #eee;
}
</style>
5. 组件懒加载 (异步组件) 😴
场景:有些组件很大,或者只在特定条件下(如弹窗、特定路由)才显示。
问题:一开始就把所有组件代码都加载了,会让首屏加载变慢。
解决方案:使用 Vue 3 的 defineAsyncComponent。
大白话:别一股脑把所有玩具都拿出来,先玩哪个再拿哪个。需要显示某个组件时,再去加载它的代码。
代码示例:
懒加载普通组件:
javascript
<template>
<button @click="showModal = true">打开重量级弹窗</button>
<!-- 当 showModal 为 true 时,才会去加载 HeavyModal 组件 -->
<HeavyModal v-if="showModal" @close="showModal = false" />
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const showModal = ref(false);
// 定义异步组件
const HeavyModal = defineAsyncComponent(() =>
import('./components/HeavyModal.vue')
);
// 也可以提供加载中和错误状态
// const HeavyModal = defineAsyncComponent({
// loader: () => import('./components/HeavyModal.vue'),
// loadingComponent: LoadingComponent, // 加载时显示的组件
// errorComponent: ErrorComponent, // 加载失败时显示的组件
// delay: 200, // 延迟显示 loadingComponent 200ms
// timeout: 3000 // 超时时间
// });
</script>
路由懒加载 (配合 Vue Router): 和 Vue 2 类似,依然推荐!
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/HomeView.vue')
},
{
path: '/about',
name: 'About',
// 路由懒加载
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
},
{
path: '/profile',
name: 'Profile',
// 路由懒加载
component: () => import(/* webpackChunkName: "profile" */ '../views/ProfileView.vue')
}
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
6. 避免不必要的响应式开销 📉
场景:你从后端获取了一个巨大的、层级很深的对象或数组,但你只需要展示它,不需要修改它,或者只需要修改其中一小部分。
问题:Vue 3 的 Proxy 默认会递归地把整个对象都变成响应式的,这对于非常大的数据结构可能会有性能开销(内存和初始化时间)。
解决方案:
- shallowRef:只让 .value 的赋值操作是响应式的,对象内部属性的变化不会触发更新。
- shallowReactive:只让对象的第一层属性是响应式的。
- markRaw:标记一个对象,使其永远不会被转化为 Proxy。
大白话:
- shallowRef:外面换新盒子(.value = newObj)会通知大家,但只动盒子里面的东西(obj.a = 1)不通知。
- shallowReactive:只监控第一层抽屉,深层抽屉里的东西变了,不管。
- markRaw:给这个对象贴个"免检"标签,告诉 Vue:"别管它,别追踪它的变化。"
代码示例:
javascript
<script setup>
import { ref, shallowRef, markRaw } from 'vue';
// 假设这是个从 API 获取的非常大的、只读的配置对象
const largeReadOnlyData = markRaw({ /* ...海量数据... */ });
// 只需要在整体替换时触发更新,内部细节变化不关心
const shallowList = shallowRef([ { id: 1, name: 'A' } ]);
function replaceList() {
shallowList.value = [ { id: 2, name: 'B' } ]; // 这会触发更新
}
function modifyItem() {
// 这不会触发使用 shallowList 的模板更新
if (shallowList.value[0]) {
shallowList.value[0].name = 'A modified';
}
}
// 使用 markRaw 包裹的对象不会被代理
const nonReactive = markRaw({ count: 0 });
// nonReactive.count++ // 这样修改不会触发任何 Vue 的更新
</script>
何时使用? 当你确定某个大数据结构不需要深度响应式,或者需要将外部库的实例(如地图实例、图表实例)存入 ref 或 reactive 但又不希望 Vue 去代理它时。
7. 缓存组件状态 (依然适用)
和 Vue 2 一样,用 包裹 或动态组件,可以缓存失活组件的状态,避免反复创建和销毁,特别适合 Tab 切换等场景。
xml
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['UserList', 'ProductList']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<script setup>
// 在需要被缓存的组件中定义 name 选项
// 例如 UserList.vue:
// export default {
// name: 'UserList',
// // ...
// }
</script>
Part 2:构建工具优化 - Webpack 🐢 vs Vite 🚀
你的 Vue 代码写得再好,构建工具拖后腿也不行!
Webpack 性能优化 (老牌劲旅,需要调教)
Webpack 功能强大,但配置复杂,容易变慢。
大白话核心思想:少干活,快干活,并行干活!
-
升级版本:尽量用最新稳定版的 Webpack、Node.js 和相关 Loader/Plugin,新版本通常有性能改进。
-
缩小构建范围:
- resolve.modules, resolve.extensions: 优化模块查找路径和后缀。
- module.rules 中的 include / exclude: 明确告诉 Loader 只处理哪些文件(比如只处理 src 目录下的 JS),别让它瞎找。
-
多进程/多线程:
- thread-loader: 把耗时的 Loader (如 babel-loader) 放到多带带的 worker 池里运行。 (注意:进程通信有开销,小项目可能反而变慢)
- terser-webpack-plugin (JS 压缩) / css-minimizer-webpack-plugin (CSS 压缩): 它们默认会开启并行压缩。
-
缓存:
- cache: Webpack 5 内置了强大的持久化缓存,只需开启 cache: { type: 'filesystem' },二次构建速度起飞!
- babel-loader 的 cacheDirectory: true。
- 文件名使用 [contenthash]:确保只有内容改变的文件,浏览器才会重新下载。
-
减少代码体积 (Bundle Size) :
- Code Splitting (代码分割) :通过 optimization.splitChunks 配置,把公共模块、node_modules 抽离出来,利用浏览器缓存。路由懒加载也是一种代码分割。
- Tree Shaking:确保你的代码是 ES Module 格式,并在 package.json 设置 "sideEffects": false (如果你的代码没有副作用),帮助 Webpack 移除死代码。
- 按需引入:像 Element Plus, Ant Design Vue 这样的库,使用官方推荐的按需引入插件(通常是 Babel 插件)。
- 图片/字体优化:使用 url-loader 或 Webpack 5 的 Asset Modules 处理资源,设置大小限制,小的转 Base64,大的输出文件。压缩图片。
-
使用更快的工具:
- JS 编译/压缩:考虑用 esbuild-loader 替换 babel-loader (如果不需要 Babel 的高级特性),用 esbuild 或 swc 作为 terser-webpack-plugin 的 minimizer 选项(可能需要额外配置)。swc-loader 也是 babel-loader 的一个高速替代品。
- CSS 处理:lightningcss (Parcel CSS) 据说比 css-loader + postcss-loader + cssnano 快。
-
分析 Bundle:
- 使用 webpack-bundle-analyzer 插件,可视化地看你的打包结果里,到底是谁占了地方,然后针对性优化。
Webpack 配置示例 (部分关键点) :
javascript
// webpack.config.js (简化示例)
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
entry: './src/main.js',
output: {
filename: 'js/[name].[contenthash:8].js', // 使用 contenthash
path: path.resolve(__dirname, 'dist'),
clean: true, // Wp5: 清理 dist 目录
assetModuleFilename: 'assets/[name].[contenthash:8][ext][query]' // 统一资源输出路径
},
cache: { // 开启文件系统缓存
type: 'filesystem',
buildDependencies: {
config: [__filename], // 配置文件变化时,缓存失效
},
},
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src'), // 只处理 src 下的 JS
exclude: /node_modules/,
use: [
// 'thread-loader', // 可选:多进程处理 loader
'babel-loader?cacheDirectory=true', // 开启 babel 缓存
],
},
{
test: /.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset', // Wp5: 资源模块
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于 8kb 转 base64
}
}
},
// ... 其他 loaders
]
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({ // JS 压缩
parallel: true, // 开启并行压缩
// terserOptions: { /* ... */ },
// 可选:使用 esbuild 或 swc 压缩
// minify: TerserPlugin.esbuildMinify,
// minify: TerserPlugin.swcMinify,
}),
new CssMinimizerPlugin({ // CSS 压缩
parallel: true,
}),
],
splitChunks: { // 代码分割
chunks: 'all', // 对所有类型的 chunk 进行分割
// cacheGroups: { /* ... 自定义分割规则 */ }
},
runtimeChunk: 'single', // 将运行时代码抽离成单独文件
},
plugins: [
// ... 其他插件
// new BundleAnalyzerPlugin() // 需要分析时取消注释
],
resolve: {
extensions: ['.vue', '.js', '.json'], // 减少查找后缀
alias: {
'@': path.resolve(__dirname, 'src'), // 设置别名
},
// modules: [path.resolve(__dirname, 'node_modules'), 'node_modules'] // 减少查找层级(通常默认ok)
}
};
Vite 性能优化 (天生快,稍加留意)
Vite 在开发环境下利用浏览器原生 ES Module 支持,无需打包,速度极快。生产环境使用 Rollup 打包。
大白话核心思想:开发时享受闪电速度,生产时让 Rollup 好好干活。
-
开发环境 (Dev) :
- 基本不用操心:Vite 的冷启动和热更新 (HMR) 已经非常快了。
- 依赖预构建 (optimizeDeps) :Vite 首次启动会用 esbuild 预构建 npm 依赖,提升后续加载速度。通常自动完成,但如果遇到问题,可以在 vite.config.js 的 optimizeDeps 中 include 或 exclude 特定依赖。
- 插件性能:留意你使用的 Vite 插件是否有效率问题(虽然大部分常见插件都很快)。
-
生产环境 (Build) :
- 代码分割:Rollup 默认会基于动态导入 import() 做很好的代码分割。路由懒加载是关键。
- Tree Shaking:Rollup 的 Tree Shaking 很优秀,配合 Vue 3 Composition API 效果更好。确保你的代码和依赖是 Tree-shakable 的。
- 压缩:Vite 生产构建默认使用 esbuild 进行 JS 和 CSS 压缩,速度非常快。也可以配置为使用 Terser (如果需要 Terser 的某些特定选项)。
- Polyfill:使用 @vitejs/plugin-legacy 为旧浏览器提供支持,它会自动生成兼容包和 polyfill。
- 手动 Chunks (进阶) :如果 Rollup 自动分割的 chunk 不符合你的预期(比如想把某个 UI 库单独打包),可以用 build.rollupOptions.output.manualChunks 自定义分割策略。
- 资源处理:Vite 对静态资源处理有良好支持,包括 Base64 内联、路径处理等。
- 分析 Bundle (需要插件) :使用 rollup-plugin-visualizer 插件来分析生产包的大小。
Vite 配置示例 (部分关键点) :
php
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy'; // 兼容旧浏览器
import { visualizer } from 'rollup-plugin-visualizer'; // 分析工具
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11'] // 配置兼容目标
}),
visualizer({ open: true }) // 运行 build 时自动打开分析报告
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
// target: 'es2015', // 构建目标,影响代码转换和 polyfill
// cssCodeSplit: true, // 默认开启 CSS 代码分割
// sourcemap: false, // 生产环境通常关闭 sourcemap
// minify: 'esbuild', // 默认使用 esbuild 压缩,可选 'terser'
// terserOptions: {}, // 如果 minify 为 'terser',可配置
rollupOptions: {
output: {
// 细粒度控制代码分割
manualChunks(id) {
// node_modules 单独打包
if (id.includes('node_modules')) {
// 获取包名
const packageName = id.toString().split('node_modules/')[1].split('/')[0];
// 可以将大型库单独打包
if (packageName === 'element-plus' || packageName === 'lodash') {
return packageName;
}
// 其他 node_modules 打包到 vendor chunk
return 'vendor';
}
},
// 资源文件分类打包
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
}
}
},
// optimizeDeps: { // 依赖预构建优化
// include: ['lodash-es', 'element-plus'],
// exclude: []
// }
});
Part 3: 不同脚手架可视化一下?📊
咱们画个简单的图,对比下 Webpack 和 Vite 开发时的主要流程区别:
大白话解读 Mermaid 图 :
你看,Webpack 开发时改一点代码,它可能得把好多相关的文件重新"打包"一下(B -> C),再给你。而 Vite 就像个快递员,你改了哪个文件(X),它就只处理这一个文件(Y),然后直接通过浏览器认识的"快递单"(原生 ESM)发给你(Z),省去了中间的打包环节,所以快得多!
总结收工!🎉
Vue 3 性能优化和构建工具优化是个持续的过程,但掌握了这些"大白话"技巧,你就能更有信心地去诊断和解决问题了:
- Vue 3 层面:善用 v-memo, v-once, 虚拟列表, 异步组件, , 并注意避免不必要的响应式开销。利用好 Composition API 的优势。
- Webpack 层面:缓存、多进程、缩小范围、代码分割、Tree Shaking 是关键,别忘了用 webpack-bundle-analyzer 这个"照妖镜"。
- Vite 层面:开发享受速度,生产关注 Rollup 的打包优化(代码分割、压缩、兼容性),必要时用 rollup-plugin-visualizer 分析。
别怕性能优化,动手实践起来,让你的 Vue 3 应用如丝般顺滑,让用户和自己都更开心!🥳