性能优化之实战指南:让你的 Vue 应⽤跑得飞起
作者:互联网
2026-03-08
Vue 性能优化实战指南:让你的 Vue 应⽤跑得飞起
1. 列表项 key 属性:被你误解最深的 Vue 知识点
兄弟们,key 这个属性估计是 Vue 里被误解最多的东⻄了。很多同学以为随便给个 index 就完事了,结果性能炸裂还不知道为啥。
1.1 key 的作⽤到底是什么?
Vue 的虚拟 DOM diff 算法通过 key 来判断节点是否可以复用。没有 key 或者 key 重复,Vue 会强制复用 DOM,导致性能下降甚至状态混乱。
{{ item.name }}
问题: 当你删除第一个元素时,Vue 会"以为"后面的元素只是变了位置,于是把第二个元素的 DOM 复用给第一个,第三个复用给第二个...结果输入框里的值全乱了!
{{ item.name }}
1.2 什么时候必须用 key?
{{ item.name }}
表单A
表单B
1.3 key 选择指南
// 好的 key
:key="item.id" // 唯一标识,最佳选择
:key="item.uuid" // 如果有 UUID 更好
:key="`${item.type}_${item.id}`" // 组合唯一标识
// 不好的 key
:key="index" // 列表会出问题
:key="Math.random()" // 每次都变,失去复用意义
:key="item.name" // 可能重复
1.4 小贴士
- 列表只有渲染,不会增删改查,用
index也问题不大 - 列表会动态变化,必须用唯一标识
- 表格、聊天、购物车这种场景,
key选错了会出大问题 - 调试时可以用 Vue DevTools 看 diff 结果,
key对不对一目了然
2. 架构级优化:从源头解决性能问题
前面讲的都是"术",现在讲"道"。架构级优化能让你的应用从根本上快起来。
2.1 代码分割:把大蛋糕切成小块
现代打包工具(Webpack、Vite)都支持代码分割,把代码拆成多个小块,按需加载。
2.1.1 路由级别代码分割
这是最常见的优化方式,每个路由一个 chunk。
// 一次性加载所有路由组件
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import Profile from '@/views/Profile.vue'
import Settings from '@/views/Settings.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/profile', component: Profile },
{ path: '/settings', component: Settings }
]
// 路由懒加载
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
},
{
path: '/profile',
component: () => import('@/views/Profile.vue')
},
{
path: '/settings',
component: () => import('@/views/Settings.vue')
}
]
打包效果:
- 首屏只加载
home.js - 用户访问
/about时才加载about.js - 首屏体积从 2MB 降到 300KB,首屏时间缩短 60%+
2.1.2 组件级别代码分割
某些大型组件(如富文本编辑器、图表库)可以按需加载。
2.1.3 动态导入
更灵活的按需加载方式。
// 点击按钮时才加载某个模块
async function loadFeature() {
if (needsAdvancedFeatures) {
const { default: AdvancedModule } = await import('@/features/advanced')
AdvancedModule.init()
}
}
// 根据条件加载不同的实现
async function getChartLibrary() {
if (useECharts) {
const echarts = await import('echarts')
return echarts
} else {
const chartjs = await import('chart.js')
return chartjs
}
}
2.1.4 第三方库分割
某些第三方库可以单独打包。
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
priority: 10
},
elementUI: {
test: /[\/]node_modules[\/]element-ui[\/]/,
name: 'elementUI',
priority: 20
},
commons: {
name: 'commons',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}
2.2 路由级别优化
除了代码分割,路由本身也有优化空间。
2.2.1 路由懒加载 + 预加载
// 路由配置
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import(/* webpackPrefetch: true */ '@/views/About.vue')
},
{
path: '/profile',
component: () => import(/* webpackPreload: true */ '@/views/Profile.vue')
}
]
区别:
webpackPrefetch:空闲时预加载,适合"可能访问"的路由webpackPreload:立即预加载,适合"即将访问"的路由
2.2.2 路由组件缓存
使用 keep-alive 缓存路由组件,避免重复渲染。
// 组件内配合使用
export default {
name: 'Home', // 必须有 name 才能被 include/exclude 匹配
data() {
return {
list: []
}
},
activated() {
// 从缓存恢复时调用
console.log('组件被激活')
this.fetchData()
},
deactivated() {
// 组件被缓存时调用
console.log('组件被停用')
}
}
2.2.3 路由守卫优化
// 重复获取数据
router.beforeEach(async (to, from, next) => {
// 每次导航都获取用户信息
const user = await fetchUser()
next()
})
// 缓存用户信息
let cachedUser = null
let lastFetchTime = 0
const CACHE_DURATION = 5 * 60 * 1000 // 5分钟
router.beforeEach(async (to, from, next) => {
const now = Date.now()
if (!cachedUser || now - lastFetchTime > CACHE_DURATION) {
cachedUser = await fetchUser()
lastFetchTime = now
}
next()
})
2.3 状态管理优化
2.3.1 Vuex 模块化
// 所有的 state 都在一个大对象里
const store = new Vuex.Store({
state: {
user: {},
products: [],
cart: [],
orders: [],
settings: {},
// ... 越来越多
}
})
// 模块化管理
const user = {
namespaced: true,
state: () => ({ currentUser: null }),
mutations: { SET_USER(state, user) { state.currentUser = user } },
actions: { async fetchUser({ commit }) { /* ... */ } }
}
const products = {
namespaced: true,
state: () => ({ list: [] }),
mutations: { SET_PRODUCTS(state, list) { state.list = list } }
}
const store = new Vuex.Store({
modules: { user, products, cart, orders }
})
2.3.2 按需注册模块
// 动态注册模块
router.beforeEach(async (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAdmin)) {
await store.registerModule('admin', adminModule)
}
next()
})
// 离开时卸载模块
router.afterEach((to, from) => {
if (!to.matched.some(record => record.meta.requiresAdmin)) {
if (store.hasModule('admin')) {
store.unregisterModule('admin')
}
}
})
2.4 组件设计原则
2.4.1 组件粒度
{{ user.name }}
{{ user.email }}
2.4.2 避免不必要的渲染
{{ staticContent }}
3. 服务端渲染 SSR:SEO 和首屏性能的双刃剑
SSR(Server-Side Rendering)能在服务器端渲染 Vue 组件,直接返回 HTML,对 SEO 和首屏加载都有巨大提升。
3.1 SSR vs CSR
| 对比项 | CSR(客户端渲染) | SSR(服务端渲染) |
|---|---|---|
| SEO | 搜索引擎爬虫难以抓取 | 直接返回 HTML,SEO 友好 |
| 首屏时间 | ️ 需要加载 JS 后才能渲染 | 首屏直接显示 HTML |
| 服务器压力 | 低,只提供静态资源 | ️ 高,需要渲染页面 |
| 开发复杂度 | 简单 | ️ 复杂,需要考虑同构 |
| 交互响应 | 客户端即时响应 | ️ 需要注水(hydration) |
3.2 Nuxt.js 快速上手
Nuxt.js 是 Vue 的 SSR 框架,开箱即用。
# 创建 Nuxt 项目
npx create-nuxt-app my-app
cd my-app
npm run dev
3.2.1 页面自动路由
pages/
├── index.vue # / 路由
├── about.vue # /about 路由
└── users/
├── index.vue # /users 路由
└── _id.vue # /users/:id 路由
3.2.2 数据获取
加载中...
{{ post.title }}
{{ post.content }}
3.2.3 SEO 优化
{{ post.title }}
3.3 Vue SSR 手动配置
如果你不想用 Nuxt,可以手动配置 Vue SSR。
3.3.1 服务端入口
// server.js
const express = require('express')
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const server = express()
server.get('*', async (req, res) => {
const app = createSSRApp({
data: () => ({ url: req.url }),
template: `访问的 URL 是:{{ url }}`
})
const appContent = await renderToString(app)
const html = `
Vue SSR
${appContent}
`
res.end(html)
})
server.listen(3000)
3.3.2 客户端入口
// client.js
import { createSSRApp } from 'vue'
import { createApp } from 'vue'
const app = createSSRApp({
data: () => ({ url: window.location.pathname }),
template: `访问的 URL 是:{{ url }}`
})
app.mount('#app')
3.4 静态站点生成(SSG)
如果你的内容是静态的,可以用静态站点生成,比 SSR 更简单。
// nuxt.config.js
export default {
// 启用静态生成
generate: {
routes: ['/post/1', '/post/2', '/post/3']
}
}
// 或者动态生成
export default {
generate: {
async routes() {
const posts = await fetchPosts()
return posts.map(post => `/post/${post.id}`)
}
}
}
3.5 SSR 性能优化
3.5.1 缓存渲染结果
const LRU = require('lru-cache')
const ssrCache = new LRU({
max: 1000,
maxAge: 1000 * 60 * 15 // 15分钟
})
async function renderPage(url) {
// 检查缓存
const cached = ssrCache.get(url)
if (cached) {
return cached
}
// 渲染页面
const html = await renderToString(app)
// 缓存结果
ssrCache.set(url, html)
return html
}
3.5.2 流式渲染
const { renderToStream } = require('@vue/server-renderer')
server.get('*', async (req, res) => {
const stream = renderToStream(app)
res.write('...')
// 流式输出
stream.pipe(res, { end: false })
stream.on('end', () => {
res.end('')
})
})
3.5.3 避免在服务端执行客户端代码
{{ window.innerWidth }}
{{ window.innerWidth }}
服务端渲染
{{ screenWidth }}
3.6 SSR 踩过的坑
3.6.1 状态同步问题
// 服务端和客户端状态不一致
export default {
async asyncData() {
// 服务端获取数据
const data = await fetchData()
return { data }
},
mounted() {
// 客户端又获取一次,可能导致冲突
this.fetchData()
}
}
// 统一状态管理
export default {
async asyncData({ store }) {
await store.dispatch('fetchData')
return { data: store.state.data }
},
computed: {
data() {
return this.$store.state.data
}
}
}
3.6.2 Cookie 处理
// 服务端访问不到 document.cookie
async function fetchUser() {
const cookie = document.cookie // 报错
}
// 通过上下文传递 cookie
async function fetchUser(context) {
const cookie = context.req.headers.cookie
// 使用 cookie 发送请求
}
3.6.3 异步组件处理
3.7 是否需要 SSR?
需要 SSR 的情况:
- 内容需要 SEO(博客、新闻、电商)
- 首屏加载时间要求极高
- 社交媒体分享需要预览卡片
不需要 SSR 的情况:
- 内部管理系统
- 社交媒体应用(如 Twitter)
- 游戏或富交互应用
总结
Vue 性能优化是一个系统工程,需要从多个层面入手:
- key 属性要选对,用唯一标识,别用 index
- 代码分割是标配,路由懒加载、组件按需加载
- 架构设计要合理,模块化、职责单一、避免过度渲染
- SSR 看场景使用,SEO 和首屏是刚需就上,否则别自找麻烦
- 监控要跟上,用 Vue DevTools、Lighthouse、Web Vitals 持续优化
最后,如果你觉得这篇⽂章对你有帮助,点个赞呗!如果觉得有问题,评论区喷我,我抗揍。
相关推荐
专题
+ 收藏
+ 收藏
+ 收藏
+ 收藏
+ 收藏
最新数据
相关文章
最新版vue3+TypeScript开发入门到实战教程之路由详解二
src-components调用链与即时聊天组件树
从0开始设计一个树和扁平数组的双向同步方案
拒绝 rem 计算!Vue3 大屏适配,我用 vfit 一行代码搞定
Home双router-view与布局切换逻辑
uniapp uview-plus 自定义动态验证
Vue3 单元测试实战:从组合式函数到组件
VUE-组件命名与注册机制
VTJ.PRO 在线应用开发平台概览
v0.dev 支持 RSC 了!AI 生成全栈组件离我们还有多远?
AI精选
