在前 6 课中,我们已经完成了 Vue 3 应用的 "从开发到上线" 全流程,得到了一个功能完整、可公开访问的待办应用。但在实际开发中,"能运行" 只是基础,"运行得快、体验流畅" 才是企业级应用的核心要求 ------ 比如页面加载慢、列表滚动卡顿、操作响应延迟等问题,都会直接影响用户体验。本节课将聚焦性能优化核心方案和进阶实战,从 "加载阶段""运行阶段""大型数据渲染" 三个关键场景切入,教你用 Vue 3 专属优化手段解决性能瓶颈,让你的应用从 "能用" 升级为 "好用、快用"。
一、课前准备:性能优化前的基础铺垫(10 分钟搞定)
优化前需要先明确 "优化目标" 和 "测试工具",避免盲目优化(先定位问题,再解决问题),新手直接照做即可:
1. 必备工具:性能监控与测试工具
- Lighthouse(Chrome 内置,核心推荐):用于全面评估应用性能(加载速度、交互流畅度等),生成可视化报告和优化建议,新手优先用这个;
- Vue DevTools(Vue 官方调试工具):查看组件渲染状态、Pinia 状态变化,定位 "不必要的组件渲染" 问题;
- Chrome 性能面板(Performance):进阶工具,用于录制页面操作流程,分析卡顿原因(比如长任务阻塞线程)。
2. 课前准备步骤
- 确保待办应用可正常运行(本地
npm run dev或线上域名); - 安装 Vue DevTools(Chrome 浏览器扩展商店搜索 "Vue DevTools",选择 Vue 3 版本);
- 用 Lighthouse 生成基础性能报告:打开 Chrome 浏览器→按 F12 打开开发者工具→切换到 Lighthouse 面板→勾选 "Performance"(性能)→点击 "Generate report"(生成报告),等待 1-2 分钟即可得到初始性能分数(满分 100,低于 80 需要优化)。
💡 核心原则:性能优化不是 "盲目调优",而是 "先测后改"------ 先通过工具定位性能瓶颈(比如是加载慢还是渲染卡),再针对性解决,避免做 "无效优化"。
3. 课前知识铺垫(通俗理解核心概念)
- 加载性能:指从输入网址到页面完全加载完成的速度,核心影响因素是 "资源体积"(JS/CSS/ 图片)和 "请求数量";
- 运行时性能:指页面加载完成后,用户操作(点击、滚动、输入)的响应速度,核心影响因素是 "不必要的组件渲染" 和 "长任务阻塞";
- 虚拟列表:针对 "大型列表渲染"(比如 1000 + 条数据)的优化方案,只渲染 "当前视口可见的列表项",大幅减少 DOM 节点数量,解决滚动卡顿问题。
二、核心实操一:加载性能优化 ------ 让页面 "秒开"
加载慢是前端应用最常见的性能问题,尤其在弱网环境下。Vue 3 项目的加载优化主要围绕 "减少资源体积""减少请求数量""优先加载关键资源" 三个方向,以下是新手可直接落地的 4 个核心方案:
1. 方案 1:路由懒加载(减少初始加载资源体积)
之前的路由配置是 "一次性加载所有页面组件",即使用户没访问的页面(比如待办详情页),也会在初始加载时被打包,导致初始资源体积过大。路由懒加载可实现 "按需加载"------ 只有用户访问某个路由时,才加载对应的组件。
实操:修改src/router/index.js,用 "动态 import" 替换直接导入:
javascript
运行
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 路由懒加载:用import(() => import('组件路径'))替代直接import
const Home = () => import('../views/Home.vue')
const TodoList = () => import('../views/TodoList.vue')
const TodoDetail = () => import('../views/TodoDetail.vue')
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/todo', name: 'TodoList', component: TodoList },
{ path: '/todo/:id', name: 'TodoDetail', component: TodoDetail }
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
效果验证:启动项目后,打开 Chrome 开发者工具→Network 面板→刷新页面,会发现初始加载时只加载了Home.vue对应的 JS 文件,点击 "进入待办列表" 后,才会加载TodoList.vue的 JS 文件,初始加载体积减少 30%-50%。
2. 方案 2:图片优化(减少图片资源体积)
图片是页面资源体积的 "大头",优化图片能快速提升加载速度,Vue 项目中推荐两种新手友好的优化方案:
(1)使用现代图片格式(WebP/AVIF)
WebP 格式比 JPG/PNG 体积小 30%-50%,且画质无损,主流浏览器都支持。实操:将项目中的图片(比如assets/logo.png)转换为 WebP 格式(用在线工具https://convertio.co/zh/png-webp/),然后在组件中使用:
vue
<template>
<div class="home">
<!-- 使用WebP格式<img src="@/assets/logo.webp" alt="Vue Logo" class="logo">
</template>
(2)图片懒加载(按需加载可视区域图片)
用 Vue 3 的v-lazy指令(需配合vue-lazyload插件)实现 "只有图片进入视口时才加载",避免初始加载所有图片。
- 安装插件:
npm install vue-lazyload -S; - 在
main.js中注册:
javascript
运行
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 引入图片懒加载插件
import VueLazyload from 'vue-lazyload'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
// 注册图片懒加载插件(可配置加载中/错误占位图)
app.use(VueLazyload, {
loading: '@/assets/loading.gif', // 加载中占位图(可选)
error: '@/assets/error.png' // 加载失败占位图(可选)
})
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
- 在组件中使用
v-lazy替代src:<template<div class<img v-lazy="require('@/assets/logo.webp')" alt="Vue Logo" class="logo</div></template>
plaintext
### 3. 方案3:第三方依赖按需引入(减少打包体积)
之前我们全局引入了Element Plus(`app.use(ElementPlus)`),会将Element Plus的所有组件都打包进项目,即使只用到了按钮、输入框等少数组件。按需引入能只打包"用到的组件",减少体积。
实操:使用Element Plus官方按需引入插件(新手直接复制步骤):
1. 安装按需引入插件:`npm install unplugin-vue-components unplugin-auto-import -D`;
2. 修改`vite.config.js`,添加插件配置:
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// 引入Element Plus按需引入插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
// 自动导入Element Plus的API(比如ElMessage)
AutoImport({
resolvers: [ElementPlusResolver()]
}),
// 自动导入Element Plus的组件
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
// 之前的打包优化配置不变...
},
server: {
// 之前的代理配置不变...
}
})
- 修改
main.js,删除全局引入 Element Plus 的代码:
javascript
运行
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './style.css'
// 删除以下3行全局引入代码
// import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css'
// import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
app.use(createPinia())
app.use(router)
// 删除全局注册Element Plus的代码
// app.use(ElementPlus)
// for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
// app.component(key, component)
// }
app.mount('#app')
效果验证:执行npm run build,对比优化前后的dist文件夹体积,会发现体积减少 20%-40%(取决于用到的组件数量)。组件使用方式不变(比如直接<el-button>`),插件会自动按需导入。
4. 方案 4:Gzip/Brotli 压缩(服务器端优化,快速见效)
Gzip/Brotli 是服务器端的压缩方案,能将 JS/CSS/HTML 等文本资源体积再压缩 50%-70%,Netlify、GitHub Pages 等免费平台默认支持,无需手动配置。
验证:部署优化后的项目到 Netlify,打开 Chrome 开发者工具→Network 面板→刷新页面,查看 JS/CSS 文件的 "Content-Encoding" 字段,若显示 "gzip" 或 "br",说明压缩生效。
三、核心实操二:运行时性能优化 ------ 让操作 "丝滑"
页面加载完成后,用户操作(点击、输入、滚动)的响应速度直接影响体验。运行时优化的核心是 "减少不必要的组件渲染" 和 "避免长任务阻塞",以下是 3 个 Vue 3 专属优化方案:
1. 方案 1:用 computed 缓存计算结果(避免重复计算)
如果组件中有频繁使用的计算逻辑(比如筛选待办列表),直接在模板中写计算逻辑会导致 "每次组件渲染都重复计算",用computed能缓存计算结果,只有依赖数据变化时才重新计算。
实操:在TodoList.vue中添加 "筛选已完成待办" 的功能,用computed缓存结果:<template><div class="todo-list-page">
<el-radio-group v-model="filterType" class="filter-group"><el-radio label</el-radio><el-radio label="completed</el-r<el-radio label="active"></el-radio</el-r<!-- 渲染筛选后的待办列表(使用computed结果<el-list class="todo-list" border :loading="todoStore.loading"><el-list-itemv-for="todo in filteredTodoList":key="todo.id"class="todo-item"<el-checkboxv-model="todo.completed"@change="(val) => todoStore.updateTodoStatus(todo.id, val)"><span :class="{ 'completed-text': todo.completed }</span></el-checkbox><el-buttontype="text"icon="Delete"class="delete-btn"@click="todoStore.deleteTodo(todo.id)"/></el-list-item><template #empty><el-empty description="暂无待办事项,快去新增吧!" /></template></el-list></div><script setup>import { ref, computed } from 'vue'import { useTodoStore } from '@/stores/todo'
const todoStore = useTodoStore ()const filterType = ref ('all') // 筛选类型:all/completed/active
// 用 computed 缓存筛选结果,只有 filterType 或 todoList 变化时才重新计算const filteredTodoList = computed (() => {switch (filterType.value) {case 'completed':return todoStore.todoList.filter (todo => todo.completed)case 'active':return todoStore.todoList.filter (todo => !todo.completed)default:return todoStore.todoList}})</script>
plaintext
⚠️ 避坑提醒:不要在computed中写"副作用逻辑"(比如修改数据、发送请求),computed是"纯函数",只负责计算和缓存结果,副作用逻辑请放在methods或watch中。
### 2. 方案2:用v-once减少重复渲染(静态内容优化)
页面中有些内容是"静态的"(比如标题、固定提示文字),不会随数据变化而变化,但Vue默认会对这些内容进行"响应式监听",导致不必要的渲染。用`v-once`指令可标记静态内容,Vue会只渲染一次,后续不再监听和重新渲染。
实操:在`Home.vue`中给静态标题添加`v-once`:
```vue<template>
<div class="home"><!-- 静态标题,添加v-once减少<h1 v-once>Vue 多页面应用首页</h1>
<p v-once>基于Vue Router + Pinia 实战(性能优化版)</p>
<p>当前待办数量:{{ todoStore.todo</p>
<router-link to="/todo" class="btn">进入</router-link</div></template>
3. 方案 3:用 watch 监听优化响应逻辑(避免过度监听)
如果需要在数据变化时执行复杂逻辑(比如请求接口、操作 DOM),用watch监听指定数据,避免 "数据变化就触发逻辑"。Vue 3 的watch支持 "深度监听""立即执行""只监听指定属性" 等灵活配置,减少不必要的逻辑触发。
实操:在TodoDetail.vue中,监听路由参数变化时重新获取待办详情(避免页面复用导致数据不更新):<template><div class="todo-detail">
plaintext
<h2 v-once></h2<div v-if="todo" class="todo-content">
<p>{{ todo.title }}<p>状态:{{ todo.completed ? '已完成' : '未完成' }}<el-button @click="goBack"></el-button></div>
<div v-else class="empty"><p>该待办</p><router-link to="/todo">返回待办</router-link></div></template<script setup>import { ref, watch } from 'vue'import { useRoute, useRouter } from 'vue-router'import { useTodoStore } from '@/stores/todo'import { getTodoDetail } from '@/api/todo'
const route = useRoute()const router = useRouter()const todoStore = useTodoStore()const todo = ref(null)
// 初始化获取详情const fetchTodoDetail = async (id) => {try {const res = await getTodoDetail (id)todo.value = {id: res.id,title: res.title,completed: res.completed}} catch (error) {todo.value = null}}
// 监听路由参数 id 的变化,重新获取详情(只监听 id,避免过度监听)watch (() => route.params.id, // 监听的目标:路由参数 id(newId) => {if (newId) fetchTodoDetail (newId) // 新 id 存在时才执行},{ immediate: true } // 立即执行一次(页面初始化时获取详情))
const goBack = () => {router.back()}</script>
plaintext
## 四、核心实操三:大型列表优化------虚拟列表实战
如果待办列表数据量很大(比如1000+条),直接用`v-for`渲染会生成大量DOM节点,导致页面滚动卡顿、操作响应慢。虚拟列表是解决这个问题的最优方案------只渲染"当前视口可见的列表项",不管数据有多少,DOM节点数量始终保持在10-20个,大幅提升流畅度。
### 1. 实操:用vue-virtual-scroller实现虚拟列表
选择`vue-virtual-scroller`插件(Vue官方推荐,适配Vue 3),步骤如下:
1. 安装插件:`npm install vue-virtual-scroller -S`;
2. 在`main.js`中引入样式(必须引入,否则布局错乱):
```javascript
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
- 在
TodoList.vue中使用虚拟列表替换原有的el-list:
vue<template>
<div class="todo-list<!-- 其他内容不变,只修改列表渲染部分 -->
<!-- 虚拟列表:height是列表容器高度,item-size是每个列表项高度(必须指定) -->
<RecycleScroller
class="todo-virtual-list"
:items="filteredTodoList"
:item-size="50"
height="500px"
>
<template #default="{ item }<!-- 列表项内容和之前一致 --><div class="todo-item">
<el-checkbox
v-model="item.completed"
@change="(val) => todoStore.updateTodoStatus(item.id, val)"
>
<span :class="{ 'completed-text': item.completed }</span></el-checkbox><el-button
type="text"
icon="Delete"
class="delete-btn"
@click="todoStore.deleteTodo(item.id)"
/>
</template<!-- 空<template #empty>
<el-empty description="暂无待办事项,快去新增吧!" />
</RecycleScroller>
</template<script setup>
import { ref, computed, onMounted } from 'vue'
import { useTodoStore } from '@/stores/todo'
// 引入虚拟列表组件
import { RecycleScroller } from 'vue-virtual-scroller'
const todoStore = useTodoStore()
const filterType = ref('all')
// 筛选后的待办列表(computed缓存不变)
const filteredTodoList = computed(() => {
switch (filterType.value) {
case 'completed':
return todoStore.todoList.filter(todo => todo.completed)
case 'active':
return todoStore.todoList.filter(todo => !todo.completed)
default:
return todoStore.todoList
}
})
// 页面加载时获取更多数据(模拟1000条数据,测试虚拟列表效果)
onMounted(() => {
// 调用Pinia中的方法,获取1000条数据(需修改api/todo.js的getTodoList,去掉slice(0,10))
todoStore.fetchTodoList()</script><style scoped>
/* 虚拟列表容器样式 */
.todo-virtual-list {
width: 100%;
border: 1px solid #eee;
border-radius: 4px;
margin: 16px 0;
}
/* 列表项样式不变 */
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
height: 50px; /* 必须和item-size一致 */
}</style>
效果验证:修改api/todo.js的getTodoList方法,去掉slice(0,10),获取 1000 条测试数据。启动项目后,滚动待办列表,会发现滚动流畅无卡顿,打开 Chrome 开发者工具→Elements 面板,查看 DOM 节点数量,始终保持在 20 个左右,优化效果显著。
五、综合实战:优化待办应用并验证性能提升
1. 实战目标
- 整合本节课所有优化方案:路由懒加载、图片优化、Element Plus 按需引入、computed 缓存、虚拟列表;
- 用 Lighthouse 测试优化前后的性能分数,验证优化效果(目标:性能分数从低于 80 提升到 90+);
- 测试用户体验:页面加载速度、列表滚动流畅度、操作响应速度是否明显提升。
2. 完整流程测试
- 本地执行
npm run lint:fix,修复代码规范问题; - 执行
npm run build,查看优化后的打包体积(对比优化前减少 50% 以上); - 用 Lighthouse 生成优化后的性能报告,查看分数提升(重点关注 "First Contentful Paint" 首次内容绘制、"Largest Contentful Paint" 最大内容绘制、"Interaction to Next Paint" 交互到下一次绘制);
- 部署优化后的项目到 Netlify,测试线上效果:
- 页面加载:输入域名后,1-2 秒内完成加载,无白屏等待;
- 列表滚动:1000 条待办数据滚动流畅,无卡顿;
- 操作响应:点击筛选、新增、删除待办,响应及时,无延迟。
3. 新手优化建议
- 进阶优化:使用
requestIdleCallback处理非紧急任务(比如统计数据上报),避免阻塞主线程; - 性能监控:集成 Sentry(前端错误监控工具),实时监控线上应用的性能问题(比如卡顿、加载失败);
- 首屏优化:针对首屏添加 "骨架屏"(Element Plus 有现成的
ElSkeleton组件),减少用户等待焦虑。
六、本节课总结与下节课预告
1. 本节课核心收获
- 加载性能优化:掌握路由懒加载、图片优化、第三方依赖按需引入,减少资源体积和请求数量;
- 运行时优化:掌握 computed 缓存、v-once、watch 监听优化,减少不必要的组件渲染和重复计算;
- 大型列表优化:掌握虚拟列表的使用,解决大量数据渲染的卡顿问题;
- 优化思维:理解 "先测后改" 的核心原则,学会用工具定位性能瓶颈,避免无效优化。
2. 课后作业(必做)
- 独立完成待办应用的所有性能优化,对比优化前后的打包体积和加载速度;
- 用 Lighthouse 生成优化前后的两份性能报告,分析分数差异和优化点;
- 实现 "骨架屏" 功能,在待办列表加载时显示骨架屏,优化首屏体验;
- 整理性能优化踩坑笔记,比如 "虚拟列表 item-size 设置错误导致布局错乱""按需引入插件配置错误导致组件失效" 等。
3. 下节课预告
下节课我们将学习 "Vue 3 项目实战拓展 ------ 从待办到全功能任务管理系统",整合前面所有课程的知识,新增用户登录、任务分类、数据统计、权限控制等企业级功能,带你完成一个 "可放入简历的完整项目",同时学习项目复盘和简历撰写技巧,为你的前端求职铺路!