Vue 项目优化实践

Vue 项目中常用的性能优化方法和代码示例

问题

在实际工作中,你对 Vue 做过哪些优化?

解答

1. 代码层面优化

v-if 与 v-show 的选择

<template>
  <!-- 频繁切换用 v-show,只是 CSS 切换 -->
  <div v-show="isVisible">频繁切换的内容</div>
  
  <!-- 条件很少改变用 v-if,真正的条件渲染 -->
  <HeavyComponent v-if="isLoaded" />
</template>

v-for 使用唯一 key

<template>
  <!-- 使用唯一标识作为 key,避免使用 index -->
  <div v-for="item in list" :key="item.id">
    {{ item.name }}
  </div>
</template>

使用 computed 缓存计算结果

<script setup>
import { ref, computed } from 'vue'

const list = ref([1, 2, 3, 4, 5])

// computed 会缓存结果,依赖不变时不会重新计算
const filteredList = computed(() => {
  return list.value.filter(item => item > 2)
})
</script>

使用 keep-alive 缓存组件

<template>
  <!-- 缓存组件状态,避免重复渲染 -->
  <keep-alive :include="['Home', 'User']" :max="10">
    <router-view />
  </keep-alive>
</template>

2. 路由懒加载

// router/index.js
const routes = [
  {
    path: '/home',
    // 动态导入,按需加载
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    // 使用 webpackChunkName 指定打包名称
    component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
  }
]

3. 组件异步加载

<script setup>
import { defineAsyncComponent } from 'vue'

// 异步加载重型组件
const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
  timeout: 3000
})
</script>

4. 虚拟滚动处理长列表

<template>
  <!-- 使用 vue-virtual-scroller 处理大数据列表 -->
  <RecycleScroller
    class="scroller"
    :items="largeList"
    :item-size="50"
    key-field="id"
  >
    <template #default="{ item }">
      <div class="item">{{ item.name }}</div>
    </template>
  </RecycleScroller>
</template>

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

// 假设有 10000 条数据
const largeList = ref([...])
</script>

5. 图片懒加载

<template>
  <!-- 使用 loading="lazy" 原生懒加载 -->
  <img 
    v-for="img in images" 
    :key="img.id"
    :src="img.url" 
    loading="lazy"
    alt=""
  />
</template>

<!-- 或使用 v-lazy 指令 -->
<script setup>
// 自定义懒加载指令
const vLazy = {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        el.src = binding.value
        observer.unobserve(el)
      }
    })
    observer.observe(el)
  }
}
</script>

<template>
  <img v-lazy="imageUrl" alt="" />
</template>

6. 防抖与节流

<script setup>
import { ref } from 'vue'
import { useDebounceFn, useThrottleFn } from '@vueuse/core'

const searchText = ref('')

// 搜索防抖,停止输入 300ms 后执行
const handleSearch = useDebounceFn((value) => {
  console.log('搜索:', value)
}, 300)

// 滚动节流,每 200ms 最多执行一次
const handleScroll = useThrottleFn(() => {
  console.log('滚动位置:', window.scrollY)
}, 200)
</script>

<template>
  <input v-model="searchText" @input="handleSearch(searchText)" />
</template>

7. 打包优化配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    // 分包策略
    rollupOptions: {
      output: {
        manualChunks: {
          // 第三方库单独打包
          'vendor': ['vue', 'vue-router', 'pinia'],
          'element': ['element-plus'],
          'echarts': ['echarts']
        }
      }
    },
    // 启用 gzip 压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // 移除 console
        drop_debugger: true  // 移除 debugger
      }
    }
  }
})

8. 使用 shallowRef/shallowReactive

<script setup>
import { shallowRef, shallowReactive } from 'vue'

// 大型对象只需要浅层响应式
const bigData = shallowRef({
  // 大量嵌套数据...
})

// 只有第一层属性是响应式的
const state = shallowReactive({
  list: [],  // list 本身响应式,但 list 内部元素不是
  config: {} 
})
</script>

关键点

  • v-if/v-show:频繁切换用 v-show,条件稳定用 v-if
  • 路由懒加载:使用动态 import 按需加载页面组件
  • 虚拟滚动:长列表使用虚拟滚动,只渲染可视区域
  • 合理使用响应式:大型静态数据用 shallowRef 或 Object.freeze
  • 打包分割:第三方库单独打包,利用浏览器缓存