Skip to content

Vue.js 性能优化

为什么需要性能优化?

性能优化可以提升用户体验,减少加载时间,降低服务器负担。本章介绍 Vue 应用的各种性能优化技巧。

1. 代码分割和懒加载

路由懒加载

javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: () => import('../views/Home.vue')  // 懒加载
    },
    {
      path: '/about',
      component: () => import('../views/About.vue')
    }
  ]
})

组件懒加载

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

// 懒加载组件
const HeavyComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)
</script>

2. 虚拟滚动

对于大列表,使用虚拟滚动只渲染可见区域:

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

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `项目 ${i}`
})))

const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)

const scrollTop = ref(0)

const visibleItems = computed(() => {
  const startIndex = Math.floor(scrollTop.value / itemHeight)
  const endIndex = startIndex + visibleCount
  return items.value.slice(startIndex, endIndex + 1)
})

const offsetY = computed(() => {
  return Math.floor(scrollTop.value / itemHeight) * itemHeight
})

function handleScroll(event) {
  scrollTop.value = event.target.scrollTop
}
</script>

<template>
  <div 
    class="virtual-scroll-container"
    :style="{ height: containerHeight + 'px' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: items.length * itemHeight + 'px', position: 'relative' }">
      <div 
        :style="{ transform: `translateY(${offsetY}px)` }"
        class="items-container"
      >
        <div
          v-for="item in visibleItems"
          :key="item.id"
          class="item"
          :style="{ height: itemHeight + 'px' }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-scroll-container {
  overflow-y: auto;
  border: 1px solid #ddd;
}

.item {
  display: flex;
  align-items: center;
  padding: 0 15px;
  border-bottom: 1px solid #eee;
}
</style>

3. 使用 v-memo 优化渲染

v-memo 可以缓存模板的一部分:

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

const items = ref([
  { id: 1, name: '项目 1', selected: false },
  { id: 2, name: '项目 2', selected: false },
  { id: 3, name: '项目 3', selected: false }
])
</script>

<template>
  <div v-for="item in items" :key="item.id" v-memo="[item.selected]">
    <!-- 只有当 item.selected 变化时才重新渲染 -->
    <input type="checkbox" v-model="item.selected" />
    <span>{{ item.name }}</span>
  </div>
</template>

4. 使用 shallowRef 和 shallowReactive

对于大型对象,使用浅层响应式可以提升性能:

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

// 只有根级别的属性是响应式的
const state = shallowReactive({
  user: {
    name: '张三',
    profile: {
      age: 25,
      city: '北京'
    }
  }
})

// 只有 .value 的变化是响应式的
const data = shallowRef({
  items: [/* 大量数据 */]
})

// 更新整个对象
function updateData() {
  data.value = { items: [/* 新数据 */] }
}
</script>

5. 计算属性缓存

合理使用计算属性,避免重复计算:

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

const items = ref([/* 大量数据 */])

// ✅ 使用计算属性 - 有缓存
const filteredItems = computed(() => {
  console.log('计算 filteredItems')
  return items.value.filter(item => item.active)
})

// ❌ 使用方法 - 无缓存
function getFilteredItems() {
  console.log('调用 getFilteredItems')
  return items.value.filter(item => item.active)
}
</script>

<template>
  <div>
    <!-- 多次访问只计算一次 -->
    <p>{{ filteredItems.length }}</p>
    <p>{{ filteredItems.length }}</p>
    
    <!-- 每次访问都会重新计算 -->
    <p>{{ getFilteredItems().length }}</p>
    <p>{{ getFilteredItems().length }}</p>
  </div>
</template>

6. 使用 v-once 和 v-memo

v-once - 只渲染一次

vue
<template>
  <div v-once>
    <!-- 这部分内容只渲染一次,不会更新 -->
    <h1>{{ staticTitle }}</h1>
    <p>{{ staticContent }}</p>
  </div>
</template>

v-memo - 条件缓存

vue
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
    <!-- 只有 item.selected 变化时才重新渲染 -->
    {{ item.name }}
  </div>
</template>

7. 避免不必要的组件更新

使用 v-show 代替 v-if

vue
<template>
  <!-- 频繁切换使用 v-show -->
  <div v-show="isVisible">内容</div>
  
  <!-- 很少改变使用 v-if -->
  <div v-if="isLoggedIn">用户信息</div>
</template>

使用 key 优化列表渲染

vue
<template>
  <!-- ✅ 使用唯一 key -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>
  
  <!-- ❌ 使用索引作为 key(列表会变化时) -->
  <div v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </div>
</template>

8. 事件处理优化

使用事件委托

vue
<script setup>
function handleClick(event) {
  if (event.target.matches('.item')) {
    console.log('点击了项目')
  }
}
</script>

<template>
  <!-- ✅ 事件委托 - 只绑定一个事件 -->
  <div @click="handleClick">
    <div class="item">项目 1</div>
    <div class="item">项目 2</div>
    <div class="item">项目 3</div>
  </div>
  
  <!-- ❌ 每个元素都绑定事件 -->
  <div>
    <div class="item" @click="handleItemClick">项目 1</div>
    <div class="item" @click="handleItemClick">项目 2</div>
    <div class="item" @click="handleItemClick">项目 3</div>
  </div>
</template>

防抖和节流

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

// 防抖
function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 节流
function throttle(fn, delay) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

const searchQuery = ref('')

// 防抖搜索
const handleSearch = debounce((query) => {
  console.log('搜索:', query)
}, 500)

// 节流滚动
const handleScroll = throttle(() => {
  console.log('滚动位置:', window.scrollY)
}, 200)
</script>

<template>
  <input 
    v-model="searchQuery" 
    @input="handleSearch(searchQuery)"
    placeholder="搜索..."
  />
</template>

9. 图片优化

懒加载图片

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

const images = ref([
  'https://picsum.photos/300/200?random=1',
  'https://picsum.photos/300/200?random=2',
  'https://picsum.photos/300/200?random=3'
])

onMounted(() => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target
        img.src = img.dataset.src
        observer.unobserve(img)
      }
    })
  })
  
  document.querySelectorAll('img[data-src]').forEach(img => {
    observer.observe(img)
  })
})
</script>

<template>
  <div class="image-gallery">
    <img
      v-for="(src, index) in images"
      :key="index"
      :data-src="src"
      src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 200'%3E%3C/svg%3E"
      alt="图片"
    />
  </div>
</template>

使用 WebP 格式

vue
<template>
  <picture>
    <source srcset="image.webp" type="image/webp" />
    <source srcset="image.jpg" type="image/jpeg" />
    <img src="image.jpg" alt="图片" />
  </picture>
</template>

10. 生产环境优化

Vite 配置

javascript
// 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'],
          'ui': ['element-plus']
        }
      }
    },
    // 压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // 移除 console
        drop_debugger: true
      }
    }
  }
})

11. 使用 Web Workers

javascript
// worker.js
self.addEventListener('message', (e) => {
  const result = heavyComputation(e.data)
  self.postMessage(result)
})

function heavyComputation(data) {
  // 耗时计算
  return data * 2
}
vue
<script setup>
import { ref } from 'vue'

const result = ref(null)
const worker = new Worker(new URL('./worker.js', import.meta.url))

worker.addEventListener('message', (e) => {
  result.value = e.data
})

function startComputation() {
  worker.postMessage(1000000)
}
</script>

12. 性能监控

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

onMounted(() => {
  // 监控首次内容绘制
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('FCP:', entry.startTime)
    }
  })
  
  observer.observe({ entryTypes: ['paint'] })
  
  // 监控长任务
  const longTaskObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log('Long task:', entry.duration)
    }
  })
  
  longTaskObserver.observe({ entryTypes: ['longtask'] })
})
</script>

性能优化清单

开发阶段

  • ✅ 使用计算属性缓存
  • ✅ 合理使用 v-if 和 v-show
  • ✅ 为列表使用唯一 key
  • ✅ 避免在模板中使用复杂表达式
  • ✅ 使用事件委托
  • ✅ 防抖和节流

构建阶段

  • ✅ 代码分割和懒加载
  • ✅ Tree Shaking
  • ✅ 压缩代码
  • ✅ 移除 console
  • ✅ 使用生产环境构建

运行时

  • ✅ 虚拟滚动
  • ✅ 图片懒加载
  • ✅ 使用 Web Workers
  • ✅ 缓存 API 请求
  • ✅ 使用 CDN

总结

  • 代码分割和懒加载减少初始加载时间
  • 虚拟滚动处理大列表
  • 使用 v-memo、v-once 优化渲染
  • 计算属性提供缓存
  • 防抖节流优化事件处理
  • 图片懒加载和 WebP 格式
  • 生产环境配置优化
  • 性能监控和分析

下一步

接下来学习 测试,了解如何测试 Vue 应用。