Vue3 性能优化实战

v-once:只渲染一次

静态内容只渲染一次,后续更新忽略。

vue 复制代码
<template>
  <div class="card">
    <img :src="product.image" class="product-image" />
    <h3>{{ product.name }}</h3>
    <p v-once class="product-desc">{{ product.description }}</p>
    <span class="product-price">¥{{ product.price }}</span>
  </div>
</template>

<script setup>
const product = ref({
  name: 'iPhone 15',
  description: '最新款苹果手机,搭载A16芯片',
  price: 5999,
  image: '/iphone.png'
})
</script>

使用场景

  • 静态文案、页脚信息
  • 不变的基础数据
  • 一次性展示的数据

v-memo:记忆化渲染

选择性更新,性能接近 v-once 但更灵活。

vue 复制代码
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
    <span>{{ item.name }}</span>
    <span v-if="item.selected" class="badge">精选</span>
  </div>
</template>

<script setup>
const list = ref([
  { id: 1, name: '项目A', selected: true },
  { id: 2, name: '项目B', selected: false },
  { id: 3, name: '项目C', selected: true }
])
</script>

原理 :当 item.selected 不变时,跳过该节点的更新。

使用场景

  • 长列表中部分数据频繁变化
  • 条件性更新列表项
  • 表格特定列更新

异步组件 + Suspense

按需加载组件,提升首屏速度。

vue 复制代码
<template>
  <div class="page">
    <h1>用户详情</h1>
    <Suspense>
      <template #default>
        <UserProfile />
      </template>
      <template #fallback>
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
  </div>
</template>

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

const UserProfile = defineAsyncComponent(() => 
  import('./components/UserProfile.vue')
)
</script>

<style scoped>
.loading {
  color: #999;
  padding: 20px;
  text-align: center;
}
</style>

优势

  • 代码分割,按需加载
  • 首屏只加载必要资源
  • Suspense 处理加载状态

配合 loading 骨架屏

vue 复制代码
<template>
  <Suspense>
    <template #default>
      <DataList />
    </template>
    <template #fallback>
      <div class="skeleton-list">
        <div v-for="i in 5" :key="i" class="skeleton-item">
          <div class="skeleton-avatar"></div>
          <div class="skeleton-text">
            <div class="skeleton-line"></div>
            <div class="skeleton-line short"></div>
          </div>
        </div>
      </div>
    </template>
  </Suspense>
</template>

<style scoped>
.skeleton-item {
  display: flex;
  gap: 12px;
  padding: 16px;
  border-bottom: 1px solid #eee;
}

.skeleton-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

.skeleton-line {
  height: 14px;
  background: #f0f0f0;
  border-radius: 4px;
  margin-bottom: 8px;
}

.skeleton-line.short {
  width: 60%;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
</style>

keep-alive 缓存组件

缓存组件实例,避免重复渲染。

vue 复制代码
<template>
  <div class="app-container">
    <nav class="tab-bar">
      <button 
        v-for="tab in tabs" 
        :key="tab.id"
        :class="{ active: currentTab === tab.id }"
        @click="currentTab = tab.id"
      >
        {{ tab.name }}
      </button>
    </nav>
    
    <keep-alive :include="cachedComponents" :max="3">
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import Home from './components/Home.vue'
import About from './components/About.vue'
import User from './components/User.vue'

const tabs = [
  { id: 'Home', name: '首页' },
  { id: 'About', name: '关于' },
  { id: 'User', name: '用户' }
]

const currentTab = ref('Home')

const currentComponent = computed(() => {
  return {
    Home,
    About,
    User
  }[currentTab.value]
})

const cachedComponents = ['Home', 'About', 'User']
</script>

<style scoped>
.tab-bar {
  display: flex;
  gap: 8px;
  padding: 12px;
  border-bottom: 1px solid #eee;
}

.tab-bar button {
  padding: 8px 16px;
  border: none;
  background: #f5f5f5;
  cursor: pointer;
  border-radius: 6px;
  transition: all 0.2s;
}

.tab-bar button.active {
  background: #4CAF50;
  color: white;
}
</style>

生命周期变化

  • 首次进入:onMountedonActivated
  • 再次激活:onActivated
  • 离开时:onDeactivated
vue 复制代码
<script setup>
import { onMounted, onActivated, onDeactivated } from 'vue'

onMounted(() => {
  console.log('首次加载')
})

onActivated(() => {
  console.log('从缓存激活')  // 适合在此刷新数据
})

onDeactivated(() => {
  console.log('进入缓存')  // 适合在此保存状态
})
</script>

最佳实践

  • max 属性限制缓存数量
  • 配合 onActivated 刷新数据
  • 使用 include/exclude 控制缓存

虚拟滚动:大列表优化

只渲染可视区域内的列表项。

vue 复制代码
<template>
  <div class="virtual-list" :style="{ height: listHeight + 'px' }">
    <div 
      class="virtual-list-content" 
      :style="{ transform: `translateY(${offsetY}px)` }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        <span class="item-index">{{ item.id }}</span>
        <span class="item-title">{{ item.title }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  itemHeight: {
    type: Number,
    default: 50
  },
  listHeight: {
    type: Number,
    default: 400
  }
})

const containerRef = ref(null)
const scrollTop = ref(0)

const visibleCount = computed(() => 
  Math.ceil(props.listHeight / props.itemHeight)
)

const startIndex = computed(() => 
  Math.floor(scrollTop.value / props.itemHeight)
)

const visibleItems = computed(() => {
  const start = Math.max(0, startIndex.value - 2)
  const end = Math.min(props.items.length, start + visibleCount.value + 2)
  return props.items.slice(start, end)
})

const offsetY = computed(() => 
  startIndex.value * props.itemHeight
)

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

<style scoped>
.virtual-list {
  overflow-y: auto;
  position: relative;
}

.list-item {
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid #f0f0f0;
  position: absolute;
  width: 100%;
  box-sizing: border-box;
}

.item-index {
  color: #999;
  margin-right: 16px;
  min-width: 30px;
}

.item-title {
  flex: 1;
}
</style>

简化版:固定高度虚拟列表

vue 复制代码
<script setup>
const itemHeight = 50
const containerHeight = 400
const containerRef = ref(null)
const scrollTop = ref(0)

const visibleItems = computed(() => {
  const start = Math.floor(scrollTop.value / itemHeight)
  const count = Math.ceil(containerHeight / itemHeight)
  return allItems.value.slice(start, start + count + 2).map((item, index) => ({
    ...item,
    _offset: (start + index) * itemHeight
  }))
})

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

<template>
  <div 
    ref="containerRef" 
    class="virtual-scroll"
    :style="{ height: containerHeight + 'px' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: allItems.length * itemHeight + 'px' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="scroll-item"
        :style="{ transform: `translateY(${item._offset}px)` }"
      >
        {{ item.title }}
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-scroll {
  overflow-y: auto;
}

.scroll-item {
  position: absolute;
  height: 50px;
  width: 100%;
  display: flex;
  align-items: center;
  padding: 0 16px;
  box-sizing: border-box;
  border-bottom: 1px solid #f5f5f5;
}
</style>

优化场景对照

场景 推荐方案
静态文案 v-once
条件性列表更新 v-memo
组件懒加载 defineAsyncComponent + Suspense
Tab 切换缓存 keep-alive
万级长列表 虚拟滚动

性能提升效果

优化点 渲染提升 内存优化
v-once 减少重复渲染 降低响应式开销
v-memo 跳过无关更新 减少 DOM 操作
异步组件 首屏更快 代码按需加载
keep-alive 切换无需重建 复用组件实例
虚拟滚动 O(n) → O(1) 只渲染可视区
相关推荐
css趣多多3 小时前
vue2项目改造为vue3遇到的问题以及解决办法
前端·vue.js·elementui
哆啦A梦15883 小时前
Vue3魔法手册 作者 张天禹 09_props的使用
前端·vue.js·typescript
山峰哥3 小时前
SQL调优实战:从索引失效到性能飙升的破局之道
服务器·数据库·sql·性能优化·编辑器·深度优先
哆啦A梦15883 小时前
Vue3魔法手册 作者 张天禹 11_自定义hooks
前端·vue.js·typescript
广州华水科技3 小时前
单北斗变形监测在大坝安全和地质灾害预警中的应用与优势
前端
一叶星殇3 小时前
Windows 下用 Nginx 部署 Vue + .NET WebApi 全流程实战
vue.js·windows·nginx
阿珊和她的猫3 小时前
深入理解 Vue 3 的 `setup` 函数
前端·vue.js·状态模式
weixin199701080164 小时前
微店商品详情页前端性能优化实战
前端·性能优化
feasibility.4 小时前
打造AI+准SaaS:中文法律检索分析平台
vue.js·人工智能·自然语言处理·django·sass·web·法律