组件懒加载与异步组件
首屏不需要的组件不应该阻塞页面加载——视频播放器、富文本编辑器、图表组件这些"重量级"内容,应该在需要时才加载。Nuxt4 提供了从
Lazy前缀到defineAsyncComponent、再到延迟水合(Lazy Hydration)的完整懒加载方案。本章讲清每种方式的原理、适用场景和性能收益。
1. LazyXxx 前缀懒加载原理
1.1 Lazy 前缀的工作方式
Nuxt4 为每个自动导入的组件自动生成一个 Lazy 前缀的版本:
<!-- 普通加载:组件代码包含在页面 JS bundle 中 -->
<VideoPlayer :url="video.url" />
<!-- 懒加载:组件代码单独打包,使用时才下载 -->
<LazyVideoPlayer :url="video.url" />底层机制:
// Nuxt 自动为每个组件生成两个版本:
// 1. VideoPlayer → import VideoPlayer from '~/components/VideoPlayer.vue'
// 2. LazyVideoPlayer → defineAsyncComponent(() => import('~/components/VideoPlayer.vue'))1.2 打包差异
不使用 Lazy:
page-bundle.js (500KB)
├── 页面逻辑 (50KB)
├── VideoPlayer (200KB) ← 首屏不需要也要下载
└── CommentList (250KB) ← 首屏不需要也要下载
使用 Lazy:
page-bundle.js (50KB) ← 首屏 JS 大幅减小
VideoPlayer.chunk.js (200KB) ← 用到时才下载
CommentList.chunk.js (250KB) ← 用到时才下载
1.3 条件渲染 + Lazy
Lazy 组件与 v-if 配合使用效果最佳——只有条件为 true 时才下载组件代码:
<template>
<div>
<!-- 点击按钮才加载播放器 -->
<button @click="showPlayer = true">播放视频</button>
<LazyVideoPlayer v-if="showPlayer" :url="video.url" />
<!-- 滚动到底部才加载评论 -->
<LazyCommentList v-if="isBottomVisible" :video-id="video.id" />
<!-- Tab 切换才加载对应内容 -->
<LazyVideoAnalytics v-if="activeTab === 'analytics'" />
</div>
</template>1.4 Lazy 的加载时机
| 场景 | 行为 |
|---|---|
<LazyXxx /> 始终渲染 | 页面加载后立即异步下载(不阻塞首屏) |
<LazyXxx v-if="false" /> | 不下载,直到条件变为 true |
<LazyXxx v-show="false" /> | 立即下载(v-show 不影响挂载) |
2. defineAsyncComponent 与 Suspense
2.1 defineAsyncComponent 基础
当需要更精细的控制时,使用 Vue 原生的 defineAsyncComponent:
const HeavyEditor = defineAsyncComponent({
// 加载函数
loader: () => import('~/components/HeavyEditor.vue'),
// 加载中显示
loadingComponent: EditorSkeleton,
delay: 200, // 200ms 后才显示 loading(避免闪烁)
// 加载失败显示
errorComponent: EditorError,
timeout: 10000, // 10 秒超时
// 加载失败时的钩子
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry() // 自动重试 3 次
} else {
fail()
}
},
})2.2 与 Suspense 配合
<template>
<Suspense>
<!-- 异步组件(可以有 async setup) -->
<HeavyDashboard />
<!-- 加载中的 fallback -->
<template #fallback>
<DashboardSkeleton />
</template>
</Suspense>
</template>
<script setup lang="ts">
const HeavyDashboard = defineAsyncComponent(
() => import('~/components/studio/Dashboard.vue')
)
</script>2.3 NuxtPage 内置 Suspense
Nuxt 的 <NuxtPage> 已经内置了 Suspense 支持:
<!-- app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
<!-- NuxtPage 自动处理页面组件的 async setup -->
<!-- 页面切换时显示加载状态 -->
</NuxtLayout>
</template>3. 延迟水合策略
3.1 什么是延迟水合
默认情况下,SSR 渲染的组件在客户端会立即全部水合。延迟水合允许某些组件推迟 Hydration 时机:
默认行为:
HTML 渲染 → JS 下载 → 所有组件同时 Hydrate → 可交互
延迟水合:
HTML 渲染 → JS 下载 → 关键组件先 Hydrate → 可交互
→ 非关键组件稍后 Hydrate(空闲/可见/事件触发)
3.2 Nuxt4 延迟水合指令
<template>
<!-- 空闲时水合(requestIdleCallback) -->
<LazyVideoRecommendations hydrate-on-idle />
<!-- 进入视口时水合(IntersectionObserver) -->
<LazyCommentList hydrate-on-visible />
<!-- 用户交互时水合(click/focus/hover) -->
<LazyVideoPlayer hydrate-on-interaction />
<!-- 媒体查询匹配时水合 -->
<LazySidebar hydrate-on-media-query="(min-width: 768px)" />
<!-- 永不水合(纯静态展示) -->
<LazyStaticBanner :hydrate="false" />
</template>3.3 各策略详解
| 策略 | 触发时机 | 适用场景 |
|---|---|---|
hydrate-on-idle | 浏览器空闲时 | 非关键交互组件 |
hydrate-on-visible | 进入视口时 | 折叠内容、底部评论 |
hydrate-on-interaction | 用户交互时 | 视频播放器(点击才需要) |
hydrate-on-media-query | 媒体查询匹配时 | 仅桌面端的侧边栏 |
:hydrate="false" | 永不水合 | 纯展示的静态内容 |
3.4 延迟水合的性能收益
首页加载(无延迟水合):
Hydration 时间:120ms(所有组件)
TTI:800ms
首页加载(有延迟水合):
Hydration 时间:40ms(仅关键组件)
TTI:500ms(-37.5%)
折叠组件:滚动到时才水合
侧边栏:桌面端才水合
4. AI 视频播放器按需加载与骨架屏
4.1 完整实战方案
<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
const showPlayer = ref(false)
</script>
<template>
<div class="max-w-6xl mx-auto">
<!-- 视频封面 / 播放器区域 -->
<div class="aspect-video bg-black rounded-xl overflow-hidden">
<template v-if="showPlayer">
<LazyVideoPlayer
:url="video.url"
:poster="video.thumbnail"
@ended="showPlayer = false"
/>
</template>
<template v-else>
<!-- 点击封面开始播放(此时才加载播放器组件) -->
<div
class="relative w-full h-full cursor-pointer group"
@click="showPlayer = true"
>
<img :src="video.thumbnail" :alt="video.title" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/40 transition">
<PlayIcon class="w-16 h-16 text-white" />
</div>
</div>
</template>
</div>
<!-- 视频信息(立即渲染) -->
<VideoInfo :video="video" />
<!-- 相关推荐(空闲时水合) -->
<LazyVideoRecommendations
:video-id="video.id"
hydrate-on-idle
/>
<!-- 评论区(进入视口时水合 + 懒加载) -->
<LazyCommentSection
:video-id="video.id"
hydrate-on-visible
/>
</div>
</template>4.2 骨架屏组件
<!-- app/components/video/PlayerSkeleton.vue -->
<template>
<div class="aspect-video bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse flex items-center justify-center">
<div class="w-16 h-16 rounded-full bg-gray-300 dark:bg-gray-700" />
</div>
</template><!-- app/components/CommentSkeleton.vue -->
<template>
<div class="space-y-4">
<div v-for="i in 3" :key="i" class="flex gap-3 animate-pulse">
<div class="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 shrink-0" />
<div class="flex-1 space-y-2">
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4" />
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2" />
</div>
</div>
</div>
</template>4.3 配合 defineAsyncComponent 的高级用法
// app/components/video/Player.client.vue 的高级加载配置
const VideoPlayer = defineAsyncComponent({
loader: () => import('~/components/video/Player.client.vue'),
loadingComponent: () => import('~/components/video/PlayerSkeleton.vue'),
delay: 100,
timeout: 15000,
onError(error, retry, fail, attempts) {
// 网络错误时重试
if (error.message.includes('fetch') && attempts <= 3) {
retry()
return
}
fail()
},
})本章小结
- Lazy 前缀:
<LazyXxx />自动将组件代码分割为独立 chunk,按需下载 - v-if + Lazy:条件为 true 时才开始下载组件代码,首屏 JS 大幅减小
- defineAsyncComponent:精细控制加载中组件、错误组件、超时、重试等
- 延迟水合:
hydrate-on-idle(空闲)、hydrate-on-visible(可见)、hydrate-on-interaction(交互) - 骨架屏:在组件加载前展示占位内容,避免布局跳动
- 实战:视频播放器点击加载 + 推荐列表空闲水合 + 评论区可见水合
下一章讲 ClientOnly 与 .client/.server 组件的运行环境隔离。