Nuxt.js 动态导入与代码分割完全指南
为什么需要代码分割
现代 Web 应用的 JavaScript 代码量越来越大。如果将所有代码打包成一个文件,会导致:
单文件打包的问题:
用户请求 → 下载巨大的 bundle.js (2MB) → 解析执行 → 页面可用
↓
等待 5-10 秒
↓
用户可能已经离开
代码分割后:
用户请求 → 下载核心代码 (200KB) → 页面基本可用
↓
等待 1-2 秒
↓
用户开始浏览
↓
后台异步加载其他代码
代码分割的收益
┌─────────────────────────────────────────────────────────────┐
│ 代码分割收益 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 更快的首屏加载 │
│ • 只加载首屏需要的代码 │
│ • 减少初始下载量 50-80% │
│ │
│ 2. 更好的缓存利用 │
│ • 代码变更只影响相关 chunk │
│ • 第三方库可独立缓存 │
│ │
│ 3. 按需加载 │
│ • 用户不访问的页面不加载 │
│ • 功能模块懒加载 │
│ │
│ 4. 并行下载 │
│ • 多个小文件可并行下载 │
│ • 利用 HTTP/2 多路复用 │
│ │
└─────────────────────────────────────────────────────────────┘
Nuxt 中的代码分割策略
Nuxt 3 默认提供了多种代码分割机制:
1. 页面级代码分割(自动)
Nuxt 自动将每个页面分割成独立的 chunk:
pages/
├── index.vue → _nuxt/pages/index.js
├── about.vue → _nuxt/pages/about.js
├── products/
│ ├── index.vue → _nuxt/pages/products/index.js
│ └── [id].vue → _nuxt/pages/products/_id.js
└── blog/
└── [...slug].vue → _nuxt/pages/blog/_...slug.js
这意味着用户只有在访问特定页面时,才会下载该页面的代码。
2. 组件动态导入
<script setup>
// 方式1:使用 defineAsyncComponent
import { defineAsyncComponent } from 'vue';
const HeavyChart = defineAsyncComponent(() =>
import('@/components/HeavyChart.vue')
);
// 方式2:带加载和错误状态
const HeavyComponent = defineAsyncComponent({
loader: () => import('@/components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // 显示加载组件前的延迟
timeout: 10000 // 超时时间
});
</script>
<template>
<div>
<HeavyChart v-if="showChart" />
<HeavyComponent />
</div>
</template>
3. Nuxt 内置的懒加载前缀
Nuxt 提供了便捷的 Lazy 前缀来实现组件懒加载:
<script setup>
// 无需手动导入,Nuxt 自动处理
// 原组件名 HeavyChart → LazyHeavyChart
</script>
<template>
<div>
<!-- 这个组件会被懒加载 -->
<LazyHeavyChart v-if="showChart" :data="chartData" />
<!-- 条件渲染配合懒加载 -->
<LazyUserDashboard v-if="user" :user="user" />
<!-- 在可视区域内才加载 -->
<LazyCommentSection />
</div>
</template>
高级动态导入模式
条件导入
根据条件决定是否导入模块:
// 仅在客户端导入
const initAnalytics = async () => {
if (process.client) {
const { init } = await import('@/lib/analytics');
init();
}
};
// 仅在特定条件下导入
const loadEditor = async () => {
if (userRole.value === 'admin') {
const { RichEditor } = await import('@/components/RichEditor.vue');
return RichEditor;
}
return null;
};
// 基于特性检测导入
const loadPolyfill = async () => {
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}
};
预加载策略
// composables/usePreload.ts
export function usePreload() {
// 预加载组件(不执行)
function preloadComponent(loader: () => Promise<any>) {
if (process.client) {
// 使用 requestIdleCallback 在空闲时预加载
requestIdleCallback(() => {
loader();
});
}
}
// 鼠标悬停时预加载
function preloadOnHover(
elementRef: Ref<HTMLElement | null>,
loader: () => Promise<any>
) {
let loaded = false;
const handleHover = () => {
if (!loaded) {
loader();
loaded = true;
}
};
onMounted(() => {
elementRef.value?.addEventListener('mouseenter', handleHover, { once: true });
});
onUnmounted(() => {
elementRef.value?.removeEventListener('mouseenter', handleHover);
});
}
// 基于路由预加载
function preloadRoute(path: string) {
const router = useRouter();
// 预加载目标路由的组件
router.resolve(path).matched.forEach(record => {
if (typeof record.components?.default === 'function') {
(record.components.default as () => Promise<any>)();
}
});
}
return {
preloadComponent,
preloadOnHover,
preloadRoute
};
}
使用预加载
<script setup>
const { preloadOnHover, preloadRoute } = usePreload();
const dashboardLink = ref(null);
// 鼠标悬停在链接上时预加载
preloadOnHover(dashboardLink, () => import('@/components/Dashboard.vue'));
// 登录成功后预加载仪表板页面
async function handleLogin() {
await loginUser();
preloadRoute('/dashboard');
router.push('/dashboard');
}
</script>
<template>
<nav>
<NuxtLink ref="dashboardLink" to="/dashboard">
仪表板
</NuxtLink>
</nav>
</template>
模块和插件的动态导入
按需导入第三方库
// composables/useChart.ts
export function useChart() {
const chartInstance = ref(null);
const isLoading = ref(false);
async function createChart(container: HTMLElement, options: any) {
isLoading.value = true;
try {
// 动态导入 ECharts
const { init } = await import('echarts');
chartInstance.value = init(container);
chartInstance.value.setOption(options);
} finally {
isLoading.value = false;
}
}
function dispose() {
chartInstance.value?.dispose();
}
onUnmounted(dispose);
return {
chartInstance,
isLoading,
createChart,
dispose
};
}
// composables/useMarkdown.ts
export function useMarkdown() {
let marked: typeof import('marked') | null = null;
async function parse(content: string): Promise<string> {
if (!marked) {
// 首次使用时才加载
marked = await import('marked');
}
return marked.parse(content);
}
return { parse };
}
动态插件加载
// plugins/analytics.client.ts
export default defineNuxtPlugin(async () => {
// 延迟加载分析库
const loadAnalytics = async () => {
const { Analytics } = await import('@/lib/analytics');
const analytics = new Analytics({
trackingId: useRuntimeConfig().public.analyticsId
});
return analytics;
};
// 页面加载完成后再初始化
if (document.readyState === 'complete') {
const analytics = await loadAnalytics();
return { provide: { analytics } };
}
return new Promise((resolve) => {
window.addEventListener('load', async () => {
const analytics = await loadAnalytics();
resolve({ provide: { analytics } });
}, { once: true });
});
});
路由级代码分割
自定义页面加载行为
// app/router.options.ts
import type { RouterConfig } from '@nuxt/schema';
export default <RouterConfig>{
// 自定义路由选项
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
}
if (to.hash) {
return { el: to.hash, behavior: 'smooth' };
}
return { top: 0, behavior: 'smooth' };
}
};
页面过渡与加载状态
<!-- app.vue -->
<script setup>
const nuxtApp = useNuxtApp();
const isPageLoading = ref(false);
// 监听页面加载状态
nuxtApp.hook('page:start', () => {
isPageLoading.value = true;
});
nuxtApp.hook('page:finish', () => {
isPageLoading.value = false;
});
</script>
<template>
<div>
<!-- 页面加载进度条 -->
<Transition name="fade">
<LoadingBar v-if="isPageLoading" />
</Transition>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
NuxtLoadingIndicator 组件
<!-- app.vue -->
<template>
<div>
<!-- Nuxt 内置的加载指示器 -->
<NuxtLoadingIndicator
:color="'#4f46e5'"
:height="3"
:duration="2000"
:throttle="200"
/>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
Chunk 分割策略配置
Vite 配置
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
// 代码分割策略
rollupOptions: {
output: {
// 手动分割 chunks
manualChunks: {
// 核心 Vue 依赖
'vue-core': ['vue', 'vue-router'],
// UI 组件库
'ui-lib': ['@headlessui/vue', '@heroicons/vue'],
// 工具库
'utils': ['lodash-es', 'date-fns'],
// 图表库(较大,单独分割)
'charts': ['echarts', 'chart.js']
},
// 或使用函数动态分割
manualChunks(id) {
// node_modules 中的依赖
if (id.includes('node_modules')) {
// 大型库单独打包
if (id.includes('echarts')) {
return 'vendor-echarts';
}
if (id.includes('monaco-editor')) {
return 'vendor-monaco';
}
// 其他依赖合并
return 'vendor';
}
}
}
},
// chunk 大小警告阈值
chunkSizeWarningLimit: 500 // KB
}
}
});
分析 Bundle 大小
// nuxt.config.ts
export default defineNuxtConfig({
// 开发时分析 bundle
$development: {
vite: {
plugins: [
// 可视化分析
process.env.ANALYZE && (await import('rollup-plugin-visualizer')).visualizer({
filename: './stats.html',
open: true,
gzipSize: true
})
].filter(Boolean)
}
}
});
# 运行分析
ANALYZE=true npm run build
组件岛屿(Component Islands)
Nuxt 3 的实验性功能,允许选择性地为组件启用客户端交互:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
componentIslands: true
}
});
<!-- components/HeavyInteractiveWidget.vue -->
<!-- 使用 .client 后缀表示只在客户端渲染 -->
<!-- 或在页面中使用 -->
<template>
<div>
<!-- 这个组件服务端渲染,客户端不需要 JS -->
<StaticContent />
<!-- 这个组件需要客户端交互,会被隔离加载 -->
<NuxtIsland name="InteractiveChart" :props="{ data: chartData }" />
</div>
</template>
服务端组件与客户端组件分离
服务端组件
<!-- components/ServerOnlyComponent.server.vue -->
<script setup>
// 这个组件只在服务端渲染
// 可以安全地使用服务端 API
const data = await $fetch('/api/server-data');
</script>
<template>
<div class="server-rendered">
{{ data }}
</div>
</template>
客户端组件
<!-- components/ClientOnlyComponent.client.vue -->
<script setup>
// 这个组件只在客户端运行
// 可以使用浏览器 API
import { ref, onMounted } from 'vue';
const windowWidth = ref(0);
onMounted(() => {
windowWidth.value = window.innerWidth;
window.addEventListener('resize', () => {
windowWidth.value = window.innerWidth;
});
});
</script>
<template>
<div>
当前窗口宽度: {{ windowWidth }}px
</div>
</template>
ClientOnly 组件
<template>
<div>
<!-- 服务端渲染的内容 -->
<header>网站标题</header>
<!-- 只在客户端渲染 -->
<ClientOnly>
<InteractiveMap :center="mapCenter" />
<!-- 服务端占位内容 -->
<template #fallback>
<div class="map-placeholder">
<img src="/map-preview.jpg" alt="地图预览" />
<p>地图加载中...</p>
</div>
</template>
</ClientOnly>
</div>
</template>
预获取和预加载
Link 预获取
<template>
<nav>
<!-- 默认:鼠标悬停时预获取 -->
<NuxtLink to="/about">关于我们</NuxtLink>
<!-- 禁用预获取 -->
<NuxtLink to="/heavy-page" :prefetch="false">
重型页面
</NuxtLink>
<!-- 视口内自动预获取 -->
<NuxtLink
to="/popular-page"
prefetch
prefetch-on="visibility"
>
热门页面
</NuxtLink>
</nav>
</template>
手动预获取
// composables/usePrefetch.ts
export function usePrefetch() {
const nuxtApp = useNuxtApp();
const router = useRouter();
// 预获取页面数据
async function prefetchPage(path: string) {
const route = router.resolve(path);
// 预获取页面组件
await Promise.all(
route.matched.map(async (record) => {
const component = record.components?.default;
if (typeof component === 'function') {
await (component as () => Promise<any>)();
}
})
);
// 如果页面有 asyncData,也预获取
// Nuxt 会自动缓存这些数据
}
// 预加载关键资源
function preloadAsset(url: string, as: 'script' | 'style' | 'image') {
if (process.server) return;
const link = document.createElement('link');
link.rel = 'preload';
link.href = url;
link.as = as;
if (as === 'script') {
link.crossOrigin = 'anonymous';
}
document.head.appendChild(link);
}
return {
prefetchPage,
preloadAsset
};
}
常见优化场景
场景1:大型表单页面
<!-- pages/application.vue -->
<script setup>
// 表单验证库按需加载
const FormValidator = defineAsyncComponent(() =>
import('@/components/FormValidator.vue')
);
// 只有在需要时才加载富文本编辑器
const showEditor = ref(false);
const RichEditor = defineAsyncComponent({
loader: () => import('@/components/RichEditor.vue'),
loadingComponent: EditorSkeleton
});
// 地址选择器
const AddressPicker = defineAsyncComponent(() =>
import('@/components/AddressPicker.vue')
);
</script>
<template>
<form @submit.prevent="handleSubmit">
<!-- 基础表单字段立即渲染 -->
<BasicFields v-model="formData" />
<!-- 复杂组件懒加载 -->
<LazyAddressPicker v-model="formData.address" />
<!-- 条件加载 -->
<button type="button" @click="showEditor = true">
添加详细描述
</button>
<RichEditor v-if="showEditor" v-model="formData.description" />
</form>
</template>
场景2:数据可视化仪表板
<!-- pages/dashboard.vue -->
<script setup>
// 仪表板数据
const { data: stats } = await useFetch('/api/dashboard/stats');
// 图表组件全部懒加载
const charts = {
revenue: () => import('@/components/charts/RevenueChart.vue'),
users: () => import('@/components/charts/UsersChart.vue'),
orders: () => import('@/components/charts/OrdersChart.vue'),
traffic: () => import('@/components/charts/TrafficChart.vue')
};
// 当前显示的图表
const activeCharts = ref(['revenue', 'users']);
// 预加载即将显示的图表
const { preloadComponent } = usePreload();
watch(activeCharts, (newCharts) => {
// 预加载所有活跃图表
newCharts.forEach(chartName => {
if (charts[chartName]) {
preloadComponent(charts[chartName]);
}
});
}, { immediate: true });
</script>
<template>
<div class="dashboard">
<!-- 统计卡片(立即渲染) -->
<StatsCards :stats="stats" />
<!-- 图表网格 -->
<div class="chart-grid">
<template v-for="chartName in activeCharts" :key="chartName">
<Suspense>
<component :is="defineAsyncComponent(charts[chartName])" />
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
</template>
</div>
</div>
</template>
场景3:功能模块按需加载
// composables/useFeature.ts
interface Feature {
name: string;
loader: () => Promise<any>;
enabled: boolean;
}
const features: Record<string, Feature> = {
chat: {
name: 'chat',
loader: () => import('@/modules/chat'),
enabled: true
},
analytics: {
name: 'analytics',
loader: () => import('@/modules/analytics'),
enabled: true
},
ai: {
name: 'ai',
loader: () => import('@/modules/ai-assistant'),
enabled: false // 付费功能
}
};
export function useFeature(featureName: string) {
const feature = features[featureName];
const module = ref(null);
const isLoading = ref(false);
const error = ref(null);
async function load() {
if (!feature?.enabled) {
error.value = new Error('Feature not available');
return;
}
isLoading.value = true;
try {
module.value = await feature.loader();
} catch (e) {
error.value = e;
} finally {
isLoading.value = false;
}
}
return {
module,
isLoading,
error,
load,
isEnabled: feature?.enabled ?? false
};
}
性能监控
// plugins/chunk-loading-monitor.client.ts
export default defineNuxtPlugin(() => {
// 监控 chunk 加载性能
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('_nuxt/')) {
console.log(`Chunk loaded: ${entry.name}`, {
duration: entry.duration.toFixed(2) + 'ms',
transferSize: entry.transferSize
});
// 上报慢加载
if (entry.duration > 1000) {
reportSlowChunk(entry);
}
}
}
});
observer.observe({
entryTypes: ['resource']
});
});
最佳实践总结
动态导入与代码分割最佳实践:
自动分割:
✓ 利用 Nuxt 的页面自动分割
✓ 使用 Lazy 前缀实现组件懒加载
✓ 服务端/客户端组件分离
手动优化:
✓ 大型第三方库动态导入
✓ 条件功能按需加载
✓ 配置合理的 chunk 分割策略
预加载策略:
✓ 空闲时预加载可能需要的模块
✓ 鼠标悬停预加载链接目标
✓ 利用 NuxtLink 的自动预获取
用户体验:
✓ 提供加载状态反馈
✓ 使用骨架屏占位
✓ 设置合理的加载超时
✓ 优雅处理加载失败
监控分析:
✓ 分析 bundle 大小和组成
✓ 监控 chunk 加载时间
✓ 设置性能预算
✓ 定期审查分割策略
合理的代码分割策略可以显著提升应用的加载性能。Nuxt 3 提供了丰富的内置支持,充分利用这些功能可以让你的应用更快、更流畅。


