Vue.js performance optimization

Why is performance optimization needed?

Performance optimization can improve user experience, reduce loading time, and reduce server burden. This chapter introduces various performance optimization techniques for Vue applications.

1. Code splitting and lazy loading

Lazy loading of routes

// 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')
    }
  ]
})

Lazy loading of components

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

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

2. Virtual scrolling

For large lists, use virtual scrolling to render only the visible area:

<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. Use v-memo to optimize rendering

v-memoParts of a template can be cached:

<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. Use shallowRef and shallowReactive

For large objects, using shallow reactivity can improve performance:

<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. Computed attribute cache

Use calculated properties appropriately to avoid double calculations:

<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. Use v-once and v-memo

v-once - Render only once

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

v-memo - conditional caching

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

7. Avoid unnecessary component updates

Use v-show instead of v-if

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

Use key to optimize list rendering

<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. Event processing optimization

Using event delegation

<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>

Anti-shake and throttling

<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. Image optimization

Lazy loading of images

<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>

Use WebP format

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

10. Production environment optimization

Vite configuration

// 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. Using Web Workers

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

function heavyComputation(data) {
  // 耗时计算
  return data * 2
}
<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. Performance monitoring

<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>

Performance optimization checklist

Development stage

  • ✅ Use computed property caching
  • ✅ Use v-if and v-show appropriately
  • ✅ Use unique keys for lists
  • ✅ Avoid using complex expressions in templates
  • ✅ Use event delegation
  • ✅ Anti-shake and throttling

Build phase

  • ✅ Code splitting and lazy loading
  • ✅ Tree Shaking
  • ✅ Compressed code
  • ✅ Remove console
  • ✅ Build using production environment

Runtime

  • ✅ Virtual scrolling
  • ✅ Lazy loading of images
  • ✅ Use Web Workers
  • ✅ Caching API requests
  • ✅ Use CDN

Summarize

  • Code splitting and lazy loading reduce initial load time
  • Virtual scrolling for handling large lists
  • Use v-memo and v-once to optimize rendering
  • Computed properties provide caching
  • Anti-shake throttling and optimized event processing
  • Lazy loading of images and WebP format
  • Production environment configuration optimization
  • Performance monitoring and analysis

Next step

Next, learn Testing to learn how to test Vue applications.