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>
生命周期变化:
- 首次进入:
onMounted→onActivated - 再次激活:
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) | 只渲染可视区 |