代码分割完全指南
代码分割是现代 Web 应用性能优化的关键。本文系统讲解从原理到实战的完整知识体系。
1. 代码分割的必要性
问题: 打包文件太大
// 不进行代码分割时的问题:
// 用户访问首页时下载整个 app.js (5MB)
// 但首页只需要其中 500KB
// 用户需要等待的时间:
// 5MB / 1Mbps = ~40 秒 (3G 网络)
// 完全不可接受!
代码分割的效果
| 指标 | 未分割 | 分割后 | 改善 |
|---|---|---|---|
| Initial JS | 5MB | 800KB | 84% ↓ |
| 首屏加载时间 | 8s | 2s | 75% ↓ |
| 缓存命中率 | 10% | 80% | 8x ↑ |
| 用户体验 | 很差 | 良好 | ✅ |
2. 代码分割策略
策略 1: 路由级代码分割
// React + React Router 例子
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// 动态导入,自动代码分割
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Products = lazy(() => import('./pages/Products'))
const Contact = lazy(() => import('./pages/Contact'))
// 加载中的占位符
function LoadingSpinner() {
return <div>加载中...</div>
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
// 打包后的结果:
// ✅ main.js (共享代码)
// ✅ home.chunk.js (首页代码)
// ✅ about.chunk.js (关于页)
// ✅ products.chunk.js (产品页)
// ✅ contact.chunk.js (联系页)
策略 2: 组件级懒加载
// Vue 3 + Vite 例子
import { defineAsyncComponent } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
// 使用 defineAsyncComponent 实现懒加载
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
const ComplexEditor = defineAsyncComponent(() =>
import('./components/ComplexEditor.vue')
)
export default {
components: {
HeavyChart,
ComplexEditor
},
template: `
<div>
<h1>仪表板</h1>
<!-- 只有当用户滚动到这里才会加载 HeavyChart -->
<HeavyChart />
<!-- 只有当用户需要编辑时才会加载 ComplexEditor -->
<ComplexEditor />
</div>
`
}
// 带加载状态的懒加载
const defineAsyncComponentWithLoading = (importFunc, delay = 200) => {
return defineAsyncComponent({
loader: importFunc,
loadingComponent: {
template: '<div>加载中...</div>'
},
delay,
errorComponent: {
template: '<div>加载失败</div>'
},
timeout: 10000
})
}
const LazyComponent = defineAsyncComponentWithLoading(
() => import('./HeavyComponent.vue')
)
策略 3: 第三方库分割
// 问题: 将所有依赖打包到 main.js
// ❌ main.js 包含: React, Vue, Lodash, Moment 等
// ✅ 解决: 提取第三方库到单独的 bundle
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: __dirname + '/dist'
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 提取 node_modules 代码
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
// 提取共享的业务代码
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: 'common'
}
}
}
}
}
// 打包结果:
// ✅ vendors.js (React, Vue 等, ~500KB)
// ✅ common.js (业务共享代码, ~50KB)
// ✅ main.js (应用代码, ~100KB)
策略 4: 动态导入 (Dynamic Import)
// 按需加载功能模块
class AnalyticsModule {
static instance: any = null
static async load() {
if (this.instance) return this.instance
// 只在需要时加载分析模块
const module = await import('./analytics.js')
this.instance = module.default
return this.instance
}
}
// 使用
async function trackEvent(eventName: string) {
const analytics = await AnalyticsModule.load()
analytics.track(eventName)
}
// 实际场景: 用户交互时加载
function AdvancedEditor() {
const [editor, setEditor] = useState(null)
const [showAdvanced, setShowAdvanced] = useState(false)
const handleAdvancedMode = async () => {
if (!editor) {
// 只有用户点击时才加载高级编辑器
const { AdvancedEditor } = await import('./AdvancedEditor')
setEditor(AdvancedEditor)
}
setShowAdvanced(!showAdvanced)
}
return (
<div>
<BasicEditor />
<button onClick={handleAdvancedMode}>
切换到高级模式
</button>
{showAdvanced && editor && <editor />}
</div>
)
}
3. 框架特定的分割方案
React + Next.js
// pages/products/index.tsx
import dynamic from 'next/dynamic'
// 方式 1: 动态导入组件
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <p>加载图表中...</p>,
ssr: false // 仅客户端加载
})
export default function Products() {
return (
<div>
<h1>产品页面</h1>
<HeavyChart />
</div>
)
}
// pages/api/data.ts 自动生成 API 路由
// pages/blog/[slug].tsx 自动生成动态路由
// next.config.js - 优化分割
module.exports = {
webpack: (config, { isServer }) => {
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendors',
priority: 10
}
}
return config
}
}
Vue 3 + Nuxt
// nuxt.config.ts - 自动代码分割
export default defineNuxtConfig({
// 自动分割路由
ssr: true,
// 分割配置
build: {
splitChunks: {
layouts: true,
pages: true,
commons: true
}
},
// 预加载策略
experimental: {
payloadExtraction: false
}
})
// pages/products/[id].vue
<script setup lang="ts">
// 自动代码分割的路由组件
import ProductDetail from '~/components/ProductDetail.vue'
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
</script>
<template>
<ProductDetail :product="product" />
</template>
// 自动分割结果:
// ✅ pages/index.vue → pages.index.js
// ✅ pages/products/[id].vue → pages.products._id_.js
// ✅ layouts/default.vue → layouts.default.js
4. 性能监控
监控分割效果
// 使用 Performance API 监控加载时间
function monitorChunkLoading() {
// 记录不同阶段的时间
const entries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
console.log('指标分析:')
console.log(`DNS: ${entries.domainLookupEnd - entries.domainLookupStart}ms`)
console.log(`TCP: ${entries.connectEnd - entries.connectStart}ms`)
console.log(`首字节: ${entries.responseStart - entries.requestStart}ms`)
console.log(`传输: ${entries.responseEnd - entries.responseStart}ms`)
console.log(`DOM解析: ${entries.domComplete - entries.domInteractive}ms`)
console.log(`加载完成: ${entries.loadEventEnd - entries.loadEventStart}ms`)
}
// 监控动态导入
async function trackDynamicImport(moduleName: string, importFunc: () => Promise<any>) {
const start = performance.now()
try {
const module = await importFunc()
const duration = performance.now() - start
console.log(`[${moduleName}] 加载耗时: ${duration.toFixed(2)}ms`)
// 上报到分析服务
if (duration > 1000) {
reportSlowLoad(moduleName, duration)
}
return module
} catch (error) {
console.error(`[${moduleName}] 加载失败:`, error)
reportLoadError(moduleName, error)
throw error
}
}
// 使用
const HeavyComponent = lazy(() =>
trackDynamicImport('HeavyComponent', () =>
import('./HeavyComponent')
)
)
5. 代码分割最佳实践
清单
// ✅ 代码分割检查清单
// 1. 路由级别: 每个页面单独 chunk
// ✅ 好
const About = lazy(() => import('./pages/About'))
const Contact = lazy(() => import('./pages/Contact'))
// 2. 大组件懒加载: 超过 50KB 的组件
// ✅ 好
const DataGrid = lazy(() => import('./DataGrid')) // 150KB
const RichTextEditor = lazy(() => import('./Editor')) // 200KB
// ❌ 不必要
const Button = lazy(() => import('./Button')) // 2KB, 不需要分割
// 3. 预加载策略: 合理预加载关键内容
// ✅ 用户即将进入产品页面时预加载
const handleHoverProducts = () => {
import('./pages/Products') // 预加载
}
// 4. 避免过度分割: 过多小 chunk 会增加 HTTP 请求
// ❌ 问题: 1000 个 10KB 的 chunk
// ✅ 好: 合并相关的 chunk
// 5. 监控加载时间
// ✅ 好
class ChunkLoader {
static async load(moduleName: string) {
const startTime = performance.now()
const module = await import(`./chunks/${moduleName}`)
const loadTime = performance.now() - startTime
if (loadTime > 1000) {
console.warn(`[SLOW] ${moduleName} 加载耗时 ${loadTime}ms`)
}
return module
}
}
// 6. 错误处理
// ✅ 好
const SafeLazyComponent = lazy(() =>
import('./Component').catch(err => {
console.error('加载组件失败:', err)
// 返回错误组件
return import('./ErrorComponent')
})
)
// 7. 使用 Suspense 提供良好的加载体验
// ✅ 好
<Suspense fallback={<LoadingSpinner />}>
<LargeComponent />
</Suspense>
6. Webpack 高级配置
// webpack.config.js 完整配置
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: __dirname + '/dist',
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
clean: true
},
optimization: {
minimize: true,
runtimeChunk: 'single', // 提取 webpack runtime
splitChunks: {
chunks: 'all',
maxAsyncRequests: 30,
maxInitialRequests: 30,
minSize: 20000,
maxSize: 244000, // 如果 chunk 超过 244KB,尝试进一步分割
cacheGroups: {
// 第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
enforce: true
},
// React
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react-vendors',
priority: 20
},
// UI 库
ui: {
test: /[\\/]node_modules[\\/](@mui|antd|element-plus)[\\/]/,
name: 'ui-libs',
priority: 15
},
// 共享业务代码
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: 'common'
}
}
}
},
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
}
7. 实战优化案例
案例: 大型管理后台优化
// 优化前: 单个 main.js - 8MB
// 首屏加载时间: 12 秒
// 优化策略:
// 1. 路由级分割
// 2. 提取第三方库 (React, Ant Design, etc)
// 3. 懒加载表格和图表
// 4. 动态导入编辑器
// 优化结果:
// ✅ main.js - 150KB
// ✅ react-vendors.js - 300KB
// ✅ ui-libs.js - 200KB
// ✅ route chunks - 平均 100KB
// 首屏加载时间: 3 秒 (75% 改善 ↓)
// 预加载关键路由
const preloadCriticalChunks = () => {
// 用户大多数时间在仪表板
import('./pages/Dashboard')
}
// 当用户完成初始操作后预加载其他路由
setTimeout(preloadCriticalChunks, 3000)
总结
代码分割的关键要点:
| 策略 | 场景 | 效果 |
|---|---|---|
| 路由分割 | 多页面应用 | 80-90% ↓ |
| 组件分割 | 大型组件 | 30-50% ↓ |
| 库分割 | 第三方库 | 缓存优化 |
| 动态导入 | 可选功能 | 按需加载 |


