性能优化之实战指南:让你的 Vue 应⽤跑得飞起

Vue 性能优化实战指南:让你的 Vue 应⽤跑得飞起

1. 列表项 key 属性:被你误解最深的 Vue 知识点

兄弟们,key 这个属性估计是 Vue 里被误解最多的东⻄了。很多同学以为随便给个 index 就完事了,结果性能炸裂还不知道为啥。

1.1 key 的作⽤到底是什么?

Vue 的虚拟 DOM diff 算法通过 key 来判断节点是否可以复用。没有 key 或者 key 重复,Vue 会强制复用 DOM,导致性能下降甚至状态混乱。

vue 复制代码
<!-- ❌ 错误:用 index 做 key -->
<template>
  <div>
    <div v-for="(item, index) in list" :key="index">
      {{ item.name }}
      <input v-model="item.value" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [
        { name: '张三', value: '' },
        { name: '李四', value: '' },
        { name: '王五', value: '' }
      ]
    }
  }
}
</script>

问题: 当你删除第一个元素时,Vue 会"以为"后面的元素只是变了位置,于是把第二个元素的 DOM 复用给第一个,第三个复用给第二个...结果输入框里的值全乱了!

vue 复制代码
<!-- ✅ 正确:用唯一标识做 key -->
<template>
  <div>
    <div v-for="item in list" :key="item.id">
      {{ item.name }}
      <input v-model="item.value" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [
        { id: 1, name: '张三', value: '' },
        { id: 2, name: '李四', value: '' },
        { id: 3, name: '王五', value: '' }
      ]
    }
  }
}
</script>

1.2 什么时候必须用 key?

vue 复制代码
<!-- 1. v-for 必须用 -->
<template>
  <div v-for="item in list" :key="item.id">{{ item.name }}</div>
</template>

<!-- 2. 条件渲染多个元素时建议用 -->
<template>
  <div v-if="showForm" :key="1">表单A</div>
  <div v-else :key="2">表单B</div>
</template>

1.3 key 选择指南

javascript 复制代码
// ✅ 好的 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。

javascript 复制代码
// ❌ 一次性加载所有路由组件
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 }
]
javascript 复制代码
// ✅ 路由懒加载
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 组件级别代码分割

某些大型组件(如富文本编辑器、图表库)可以按需加载。

vue 复制代码
<template>
  <div>
    <button @click="showEditor = true">打开编辑器</button>

    <!-- 条件加载大型组件 -->
    <Editor v-if="showEditor" @close="showEditor = false" />
  </div>
</template>

<script>
export default {
  components: {
    Editor: () => import('@/components/Editor.vue')
  },
  data() {
    return {
      showEditor: false
    }
  }
}
</script>
2.1.3 动态导入

更灵活的按需加载方式。

javascript 复制代码
// 点击按钮时才加载某个模块
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 第三方库分割

某些第三方库可以单独打包。

javascript 复制代码
// 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 路由懒加载 + 预加载
javascript 复制代码
// 路由配置
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 缓存路由组件,避免重复渲染。

vue 复制代码
<template>
  <div id="app">
    <!-- 缓存所有路由组件 -->
    <keep-alive>
      <router-view />
    </keep-alive>

    <!-- 或者只缓存特定路由 -->
    <keep-alive :include="['Home', 'Profile']">
      <router-view />
    </keep-alive>

    <!-- 排除某些路由 -->
    <keep-alive :exclude="['Login', 'Register']">
      <router-view />
    </keep-alive>
  </div>
</template>
javascript 复制代码
// 组件内配合使用
export default {
  name: 'Home',  // 必须有 name 才能被 include/exclude 匹配
  data() {
    return {
      list: []
    }
  },
  activated() {
    // 从缓存恢复时调用
    console.log('组件被激活')
    this.fetchData()
  },
  deactivated() {
    // 组件被缓存时调用
    console.log('组件被停用')
  }
}
2.2.3 路由守卫优化
javascript 复制代码
// ❌ 重复获取数据
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 模块化
javascript 复制代码
// ❌ 所有的 state 都在一个大对象里
const store = new Vuex.Store({
  state: {
    user: {},
    products: [],
    cart: [],
    orders: [],
    settings: {},
    // ... 越来越多
  }
})
javascript 复制代码
// ✅ 模块化管理
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 按需注册模块
javascript 复制代码
// 动态注册模块
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 组件粒度
vue 复制代码
<!-- ❌ 组件太大,职责不清 -->
<template>
  <div class="user-list">
    <div v-for="user in users" :key="user.id">
      <img :src="user.avatar">
      <div>{{ user.name }}</div>
      <div>{{ user.email }}</div>
      <button @click="follow(user)">关注</button>
      <button @click="block(user)">拉黑</button>
      <button @click="sendMessage(user)">发消息</button>
    </div>
  </div>
</template>
vue 复制代码
<!-- ✅ 拆分成多个小组件 -->
<template>
  <UserList :users="users">
    <template #default="{ user }">
      <UserCard :user="user">
        <template #actions>
          <UserActions :user="user" />
        </template>
      </UserCard>
    </template>
  </UserList>
</template>

<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <Avatar :src="user.avatar" />
    <UserInfo :name="user.name" :email="user.email" />
    <slot name="actions" />
  </div>
</template>

<!-- UserActions.vue -->
<template>
  <div class="actions">
    <button @click="$emit('follow')">关注</button>
    <button @click="$emit('block')">拉黑</button>
    <button @click="$emit('message')">发消息</button>
  </div>
</template>
2.4.2 避免不必要的渲染
vue 复制代码
<template>
  <div>
    <!-- ❌ 每次父组件更新都会重新渲染 -->
    <ExpensiveComponent :data="heavyData" />

    <!-- ✅ 使用计算属性缓存 -->
    <ExpensiveComponent :data="processedData" />

    <!-- ✅ 使用 v-once 只渲染一次 -->
    <div v-once>{{ staticContent }}</div>

    <!-- ✅ 使用 shouldComponentUpdate(Vue 2)或 computed(Vue 3) -->
    <ExpensiveComponent v-if="shouldRender" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      heavyData: largeData,
      someOtherData: []
    }
  },
  computed: {
    processedData() {
      return this.heavyData.map(item => ({
        ...item,
        formatted: this.format(item)
      }))
    },
    shouldRender() {
      return this.heavyData.length > 0
    }
  },
  methods: {
    format(item) {
      // 昂贵的计算
      return item.value.toFixed(2)
    }
  }
}
</script>

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 框架,开箱即用。

bash 复制代码
# 创建 Nuxt 项目
npx create-nuxt-app my-app

cd my-app
npm run dev
3.2.1 页面自动路由
bash 复制代码
pages/
├── index.vue          # / 路由
├── about.vue          # /about 路由
└── users/
    ├── index.vue      # /users 路由
    └── _id.vue       # /users/:id 路由
3.2.2 数据获取
vue 复制代码
<!-- pages/index.vue -->
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else>
      <h1>{{ post.title }}</h1>
      <p>{{ post.content }}</p>
    </div>
  </div>
</template>

<script>
export default {
  // 服务器端渲染前获取数据
  async asyncData({ params, $axios }) {
    const post = await $axios.$get(`/api/posts/${params.id}`)
    return { post }
  },

  // 或者在客户端获取数据
  async fetch({ store, $axios }) {
    const posts = await $axios.$get('/api/posts')
    store.commit('posts/SET_POSTS', posts)
  },

  data() {
    return {
      loading: false,
      post: {}
    }
  }
}
</script>
3.2.3 SEO 优化
vue 复制代码
<template>
  <div>
    <h1>{{ post.title }}</h1>
  </div>
</template>

<script>
export default {
  async asyncData({ $axios, params }) {
    const post = await $axios.$get(`/api/posts/${params.id}`)
    return { post }
  },

  head() {
    return {
      title: this.post.title,
      meta: [
        { hid: 'description', name: 'description', content: this.post.excerpt },
        { hid: 'og:title', property: 'og:title', content: this.post.title },
        { hid: 'og:image', property: 'og:image', content: this.post.image }
      ]
    }
  }
}
</script>

3.3 Vue SSR 手动配置

如果你不想用 Nuxt,可以手动配置 Vue SSR。

3.3.1 服务端入口
javascript 复制代码
// 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: `<div>访问的 URL 是:{{ url }}</div>`
  })

  const appContent = await renderToString(app)

  const html = `
    <!DOCTYPE html>
    <html>
      <head><title>Vue SSR</title></head>
      <body>
        <div id="app">${appContent}</div>
      </body>
    </html>
  `

  res.end(html)
})

server.listen(3000)
3.3.2 客户端入口
javascript 复制代码
// client.js
import { createSSRApp } from 'vue'
import { createApp } from 'vue'

const app = createSSRApp({
  data: () => ({ url: window.location.pathname }),
  template: `<div>访问的 URL 是:{{ url }}</div>`
})

app.mount('#app')

3.4 静态站点生成(SSG)

如果你的内容是静态的,可以用静态站点生成,比 SSR 更简单。

javascript 复制代码
// 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 缓存渲染结果
javascript 复制代码
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 流式渲染
javascript 复制代码
const { renderToStream } = require('@vue/server-renderer')

server.get('*', async (req, res) => {
  const stream = renderToStream(app)

  res.write('<!DOCTYPE html><html><head>...')

  // 流式输出
  stream.pipe(res, { end: false })

  stream.on('end', () => {
    res.end('</html>')
  })
})
3.5.3 避免在服务端执行客户端代码
vue 复制代码
<template>
  <div>
    <!-- ❌ 服务端没有 window -->
    <div>{{ window.innerWidth }}</div>

    <!-- ✅ 使用 process.client 判断 -->
    <div v-if="process.client">{{ window.innerWidth }}</div>
    <div v-else>服务端渲染</div>

    <!-- ✅ 或者在 mounted 中获取 -->
    <div>{{ screenWidth }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      screenWidth: 0
    }
  },
  mounted() {
    // mounted 只在客户端执行
    this.screenWidth = window.innerWidth
  }
}
</script>

3.6 SSR 踩过的坑

3.6.1 状态同步问题
javascript 复制代码
// ❌ 服务端和客户端状态不一致
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
    }
  }
}
javascript 复制代码
// ❌ 服务端访问不到 document.cookie
async function fetchUser() {
  const cookie = document.cookie // 报错
}

// ✅ 通过上下文传递 cookie
async function fetchUser(context) {
  const cookie = context.req.headers.cookie
  // 使用 cookie 发送请求
}
3.6.3 异步组件处理
vue 复制代码
<template>
  <div>
    <!-- SSR 时异步组件不会渲染 -->
    <AsyncComponent />
  </div>
</template>

<script>
export default {
  components: {
    // ✅ 使用 SSR 友好的异步组件
    AsyncComponent: defineAsyncComponent({
      loader: () => import('./AsyncComponent.vue'),
      loadingComponent: LoadingComponent,
      errorComponent: ErrorComponent,
      delay: 200,
      timeout: 3000
    })
  }
}
</script>

3.7 是否需要 SSR?

需要 SSR 的情况:

  • 内容需要 SEO(博客、新闻、电商)
  • 首屏加载时间要求极高
  • 社交媒体分享需要预览卡片

不需要 SSR 的情况:

  • 内部管理系统
  • 社交媒体应用(如 Twitter)
  • 游戏或富交互应用

总结

Vue 性能优化是一个系统工程,需要从多个层面入手:

  1. key 属性要选对,用唯一标识,别用 index
  2. 代码分割是标配,路由懒加载、组件按需加载
  3. 架构设计要合理,模块化、职责单一、避免过度渲染
  4. SSR 看场景使用,SEO 和首屏是刚需就上,否则别自找麻烦
  5. 监控要跟上,用 Vue DevTools、Lighthouse、Web Vitals 持续优化

最后,如果你觉得这篇⽂章对你有帮助,点个赞呗 !如果觉得有问题,评论区喷我,我抗揍。

相关推荐
徐小夕3 小时前
JitWord Office预览引擎:如何用Vue3+Node.js打造丝滑的PDF/Excel/PPT嵌入方案
前端·vue.js·github
晴殇i3 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
孟陬3 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
BER_c3 小时前
前端权限校验最佳实践:一个健壮的柯里化工具函数
前端·javascript
兆子龙3 小时前
别再用 useState / data 管 Tabs 的 activeKey 了:和 URL 绑定才香
前端·架构
sudo_jin3 小时前
前端包管理器演进史:为什么 npm 之后,Yarn 和 pnpm 成了新宠?
前端·npm
敲敲敲敲暴你脑袋4 小时前
写个添加注释的vscode插件
javascript·typescript·visual studio code
叁两4 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
golang学习记4 小时前
GitLens 十大神技:彻底改变你在 VS Code 中的 Git 工作流
前端·后端·visual studio code