Skip to content

性能优化

性能优化是构建高质量 Vue.js 应用的重要环节。本章将介绍各种性能优化策略和最佳实践。

渲染性能优化

使用 v-show vs v-if

根据切换频率选择合适的条件渲染指令:

vue
<template>
  <!-- 频繁切换使用 v-show -->
  <div v-show="isVisible">频繁切换的内容</div>
  
  <!-- 条件很少改变使用 v-if -->
  <div v-if="userRole === 'admin'">管理员内容</div>
</template>

列表渲染优化

为列表项提供唯一的 key:

vue
<template>
  <!-- 不好 -->
  <li v-for="item in items">{{ item.name }}</li>
  
  <!-- 好 -->
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</template>

避免在 v-for 中使用 v-if:

vue
<template>
  <!-- 不好 -->
  <li v-for="user in users" v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
  
  <!-- 好 -->
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</template>

<script>
computed: {
  activeUsers() {
    return this.users.filter(user => user.isActive)
  }
}
</script>

虚拟滚动

对于大量数据的列表,使用虚拟滚动:

vue
<template>
  <div class="virtual-list" @scroll="handleScroll" ref="container">
    <div :style="{ height: totalHeight + 'px' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ 
          position: 'absolute',
          top: item.top + 'px',
          height: itemHeight + 'px'
        }"
      >
        {{ item.data }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [], // 所有数据
      itemHeight: 50,
      containerHeight: 400,
      scrollTop: 0
    }
  },
  
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight
    },
    
    visibleItems() {
      const start = Math.floor(this.scrollTop / this.itemHeight)
      const end = start + Math.ceil(this.containerHeight / this.itemHeight)
      
      return this.items.slice(start, end + 1).map((item, index) => ({
        id: item.id,
        data: item,
        top: (start + index) * this.itemHeight
      }))
    }
  },
  
  methods: {
    handleScroll(e) {
      this.scrollTop = e.target.scrollTop
    }
  }
}
</script>

组件优化

使用异步组件

对于大型组件或不常用的组件,使用异步加载:

javascript
// 异步组件
const AsyncComponent = defineAsyncComponent(() => 
  import('./components/HeavyComponent.vue')
)

// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

组件懒加载

使用 Intersection Observer 实现组件懒加载:

vue
<template>
  <div ref="container">
    <component :is="shouldLoad ? actualComponent : placeholderComponent" />
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
  data() {
    return {
      shouldLoad: false
    }
  },
  
  computed: {
    actualComponent() {
      return defineAsyncComponent(() => import('./HeavyComponent.vue'))
    },
    
    placeholderComponent() {
      return {
        template: '<div class="placeholder">Loading...</div>'
      }
    }
  },
  
  mounted() {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        this.shouldLoad = true
        observer.disconnect()
      }
    })
    
    observer.observe(this.$refs.container)
  }
}
</script>

使用 KeepAlive

缓存不活跃的组件实例:

vue
<template>
  <KeepAlive :include="['ComponentA', 'ComponentB']">
    <component :is="currentComponent" />
  </KeepAlive>
</template>

动态控制缓存:

vue
<template>
  <KeepAlive :max="10">
    <router-view v-if="$route.meta.keepAlive" />
  </KeepAlive>
  <router-view v-if="!$route.meta.keepAlive" />
</template>

响应式优化

使用 shallowRef 和 shallowReactive

对于大型对象,使用浅层响应式:

javascript
import { shallowRef, shallowReactive } from 'vue'

// 只有 .value 的访问会被追踪
const state = shallowRef({ count: 1 })

// 只有根级别的属性是响应式的
const state2 = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

使用 markRaw

标记对象为非响应式:

javascript
import { markRaw } from 'vue'

const foo = markRaw({
  nested: {}
})

const bar = reactive({
  // 虽然 `foo` 被标记为了 raw,但 foo.nested 不是。
  nested: foo.nested
})

避免不必要的响应式

javascript
// 不好 - 不需要响应式的数据
data() {
  return {
    expensiveData: this.processLargeDataset(),
    staticConfig: { /* 大量静态配置 */ }
  }
}

// 好 - 将静态数据移到组件外部
const STATIC_CONFIG = { /* 大量静态配置 */ }

export default {
  data() {
    return {
      expensiveData: null
    }
  },
  
  created() {
    // 在需要时才处理数据
    this.expensiveData = this.processLargeDataset()
  }
}

计算属性优化

避免在计算属性中进行副作用操作

javascript
// 不好
computed: {
  expensiveValue() {
    // 副作用操作
    this.doSomethingExpensive()
    return this.someValue * 2
  }
}

// 好
computed: {
  expensiveValue() {
    return this.someValue * 2
  }
},

watch: {
  someValue() {
    this.doSomethingExpensive()
  }
}

使用计算属性缓存

javascript
// 利用计算属性的缓存特性
computed: {
  filteredItems() {
    // 只有当 items 或 filter 改变时才重新计算
    return this.items.filter(item => 
      item.name.includes(this.filter)
    )
  }
}

事件处理优化

事件委托

对于大量相似元素的事件处理,使用事件委托:

vue
<template>
  <div @click="handleClick">
    <button data-action="save">保存</button>
    <button data-action="cancel">取消</button>
    <button data-action="delete">删除</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick(event) {
      const action = event.target.dataset.action
      if (action) {
        this[`handle${action.charAt(0).toUpperCase() + action.slice(1)}`]()
      }
    },
    
    handleSave() { /* ... */ },
    handleCancel() { /* ... */ },
    handleDelete() { /* ... */ }
  }
}
</script>

防抖和节流

对于频繁触发的事件,使用防抖或节流:

javascript
import { debounce, throttle } from 'lodash-es'

export default {
  methods: {
    // 防抖 - 搜索输入
    handleSearch: debounce(function(query) {
      this.search(query)
    }, 300),
    
    // 节流 - 滚动事件
    handleScroll: throttle(function(event) {
      this.updateScrollPosition(event)
    }, 100)
  }
}

内存优化

清理定时器和事件监听器

javascript
export default {
  data() {
    return {
      timer: null
    }
  },
  
  mounted() {
    this.timer = setInterval(() => {
      // 定时任务
    }, 1000)
    
    window.addEventListener('resize', this.handleResize)
  },
  
  beforeUnmount() {
    // 清理定时器
    if (this.timer) {
      clearInterval(this.timer)
    }
    
    // 清理事件监听器
    window.removeEventListener('resize', this.handleResize)
  }
}

避免内存泄漏

javascript
// 避免在组件中保存 DOM 引用
export default {
  data() {
    return {
      // 不好
      // domElement: null
    }
  },
  
  mounted() {
    // 不好 - 保存 DOM 引用可能导致内存泄漏
    // this.domElement = document.querySelector('.some-element')
    
    // 好 - 直接使用,不保存引用
    const element = document.querySelector('.some-element')
    element.addEventListener('click', this.handleClick)
  }
}

网络优化

请求合并

合并多个 API 请求:

javascript
// 不好 - 多个独立请求
async loadUserData() {
  const profile = await api.getUserProfile()
  const settings = await api.getUserSettings()
  const notifications = await api.getNotifications()
  
  return { profile, settings, notifications }
}

// 好 - 合并请求
async loadUserData() {
  const [profile, settings, notifications] = await Promise.all([
    api.getUserProfile(),
    api.getUserSettings(),
    api.getNotifications()
  ])
  
  return { profile, settings, notifications }
}

请求缓存

实现简单的请求缓存:

javascript
class ApiCache {
  constructor() {
    this.cache = new Map()
    this.ttl = 5 * 60 * 1000 // 5分钟
  }
  
  get(key) {
    const item = this.cache.get(key)
    if (item && Date.now() - item.timestamp < this.ttl) {
      return item.data
    }
    return null
  }
  
  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }
}

const apiCache = new ApiCache()

export default {
  methods: {
    async fetchUserData(userId) {
      const cacheKey = `user-${userId}`
      let userData = apiCache.get(cacheKey)
      
      if (!userData) {
        userData = await api.getUser(userId)
        apiCache.set(cacheKey, userData)
      }
      
      return userData
    }
  }
}

构建优化

代码分割

使用动态导入进行代码分割:

javascript
// 路由级别的代码分割
const routes = [
  {
    path: '/home',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./views/About.vue')
  }
]

// 组件级别的代码分割
export default {
  components: {
    HeavyComponent: () => import('./HeavyComponent.vue')
  }
}

Tree Shaking

确保只导入需要的模块:

javascript
// 不好 - 导入整个库
import _ from 'lodash'

// 好 - 只导入需要的函数
import { debounce, throttle } from 'lodash-es'

// 或者使用具体的模块
import debounce from 'lodash-es/debounce'

预加载关键资源

vue
<template>
  <div>
    <!-- 预加载关键图片 -->
    <link rel="preload" as="image" href="/critical-image.jpg">
    
    <!-- 预连接到外部域名 -->
    <link rel="preconnect" href="https://api.example.com">
  </div>
</template>

性能监控

使用 Performance API

javascript
// 测量组件渲染时间
export default {
  beforeMount() {
    performance.mark('component-mount-start')
  },
  
  mounted() {
    performance.mark('component-mount-end')
    performance.measure(
      'component-mount',
      'component-mount-start',
      'component-mount-end'
    )
    
    const measure = performance.getEntriesByName('component-mount')[0]
    console.log(`组件挂载耗时: ${measure.duration}ms`)
  }
}

长任务监控

javascript
// 监控长任务
if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.duration > 50) {
        console.warn(`长任务检测: ${entry.duration}ms`)
      }
    }
  })
  
  observer.observe({ entryTypes: ['longtask'] })
}

性能检查清单

  • [ ] 使用生产环境构建
  • [ ] 启用 gzip 压缩
  • [ ] 使用 CDN 分发静态资源
  • [ ] 实现代码分割和懒加载
  • [ ] 优化图片(WebP 格式、适当尺寸)
  • [ ] 使用 Service Worker 缓存
  • [ ] 减少 HTTP 请求数量
  • [ ] 避免不必要的重新渲染
  • [ ] 使用虚拟滚动处理大列表
  • [ ] 实现骨架屏提升感知性能

下一步

vue study guide - 专业的 Vue.js 学习平台