Appearance
代码分割策略
课程概述
本章节深入探讨前端应用的代码分割策略,学习如何合理拆分代码以提升应用性能。通过有效的代码分割,可以减少初始加载时间,提升用户体验。
学习目标
- 理解代码分割的概念和重要性
- 掌握不同的代码分割策略
- 学习使用动态导入实现代码分割
- 了解路由级代码分割
- 掌握组件级代码分割
- 学习库和公共代码的分割策略
第一部分:代码分割基础
1.1 什么是代码分割
代码分割(Code Splitting)是将应用代码拆分成多个bundle的技术,按需加载所需代码,而不是一次性加载整个应用。
未分割示例:
javascript
// 单个 bundle,包含所有代码
bundle.js (2MB) → 首次加载全部分割后示例:
javascript
// 多个 bundle,按需加载
main.js (200KB) → 首次加载
home.chunk.js (100KB) → 访问首页时加载
about.chunk.js (80KB) → 访问关于页时加载
admin.chunk.js (150KB) → 访问管理页时加载1.2 为什么需要代码分割
javascript
1. 减少初始加载时间
2. 优化资源利用
3. 提升页面性能
4. 改善用户体验
5. 降低带宽消耗性能对比:
javascript
// 未分割
首次加载: 2MB
加载时间: 6秒
可交互时间: 7秒
// 分割后
首次加载: 200KB
加载时间: 0.8秒
可交互时间: 1秒1.3 代码分割策略
javascript
1. 路由级分割 - 按路由拆分
2. 组件级分割 - 按组件拆分
3. 库级分割 - 第三方库单独打包
4. 功能级分割 - 按功能模块拆分
5. 时间分割 - 按加载时机拆分第二部分:动态导入
2.1 基础动态导入
typescript
// 传统静态导入
import { add } from './math'
// 动态导入
const math = await import('./math')
math.add(1, 2)
// 或使用 Promise
import('./math').then(math => {
math.add(1, 2)
})2.2 React中的动态导入
typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
// 懒加载组件
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
)
}2.3 带错误处理的动态导入
typescript
// src/components/LazyComponent.tsx
import { lazy, Suspense, ComponentType } from 'react'
function lazyWithRetry<T extends ComponentType<any>>(
componentImport: () => Promise<{ default: T }>
) {
return lazy(async () => {
const pageHasAlreadyBeenForceRefreshed = JSON.parse(
window.sessionStorage.getItem('page-has-been-force-refreshed') || 'false'
)
try {
const component = await componentImport()
window.sessionStorage.setItem('page-has-been-force-refreshed', 'false')
return component
} catch (error) {
if (!pageHasAlreadyBeenForceRefreshed) {
window.sessionStorage.setItem('page-has-been-force-refreshed', 'true')
return window.location.reload()
}
throw error
}
})
}
// 使用
const Dashboard = lazyWithRetry(() => import('./pages/Dashboard'))2.4 预加载和预获取
typescript
// 魔法注释
const Dashboard = lazy(() =>
import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./pages/Dashboard'
)
)
const AdminPanel = lazy(() =>
import(
/* webpackChunkName: "admin" */
/* webpackPreload: true */
'./pages/AdminPanel'
)
)区别:
javascript
// Prefetch - 空闲时加载
<link rel="prefetch" href="dashboard.chunk.js">
// Preload - 立即加载
<link rel="preload" href="admin.chunk.js">第三部分:路由级代码分割
3.1 React Router配置
typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { LoadingSpinner } from './components/LoadingSpinner'
// 路由级懒加载
const Home = lazy(() => import('./pages/Home'))
const Products = lazy(() => import('./pages/Products'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Cart = lazy(() => import('./pages/Cart'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Profile = lazy(() => import('./pages/Profile'))
const Admin = lazy(() => import('./pages/Admin'))
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/profile" element={<Profile />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}3.2 嵌套路由分割
typescript
// src/pages/Admin.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const AdminDashboard = lazy(() => import('./AdminDashboard'))
const AdminUsers = lazy(() => import('./AdminUsers'))
const AdminProducts = lazy(() => import('./AdminProducts'))
const AdminSettings = lazy(() => import('./AdminSettings'))
export default function Admin() {
return (
<div className="admin-layout">
<AdminSidebar />
<div className="admin-content">
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<AdminDashboard />} />
<Route path="/users" element={<AdminUsers />} />
<Route path="/products" element={<AdminProducts />} />
<Route path="/settings" element={<AdminSettings />} />
</Routes>
</Suspense>
</div>
</div>
)
}3.3 路由预加载
typescript
// src/utils/routePreload.ts
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
const routePreloadMap: Record<string, () => Promise<any>> = {
'/products': () => import('./pages/Products'),
'/cart': () => import('./pages/Cart'),
'/checkout': () => import('./pages/Checkout'),
}
export function useRoutePreload() {
const location = useLocation()
useEffect(() => {
// 预加载相关路由
const preloadRoutes = getRelatedRoutes(location.pathname)
preloadRoutes.forEach(route => {
const preload = routePreloadMap[route]
if (preload) {
setTimeout(() => preload(), 2000) // 2秒后预加载
}
})
}, [location])
}
function getRelatedRoutes(currentPath: string): string[] {
const routeMap: Record<string, string[]> = {
'/': ['/products', '/cart'],
'/products': ['/cart'],
'/cart': ['/checkout'],
}
return routeMap[currentPath] || []
}第四部分:组件级代码分割
4.1 大型组件分割
typescript
// src/pages/Dashboard.tsx
import { lazy, Suspense } from 'react'
const Chart = lazy(() => import('@/components/Chart'))
const DataTable = lazy(() => import('@/components/DataTable'))
const ReportGenerator = lazy(() => import('@/components/ReportGenerator'))
export default function Dashboard() {
return (
<div className="dashboard">
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
<Suspense fallback={<div>Loading table...</div>}>
<DataTable />
</Suspense>
<Suspense fallback={<div>Loading report...</div>}>
<ReportGenerator />
</Suspense>
</div>
)
}4.2 条件组件分割
typescript
// src/components/Editor.tsx
import { lazy, Suspense, useState } from 'react'
const RichTextEditor = lazy(() => import('./RichTextEditor'))
const MarkdownEditor = lazy(() => import('./MarkdownEditor'))
export function Editor() {
const [editorType, setEditorType] = useState<'rich' | 'markdown'>('rich')
return (
<div>
<select value={editorType} onChange={(e) => setEditorType(e.target.value as any)}>
<option value="rich">Rich Text</option>
<option value="markdown">Markdown</option>
</select>
<Suspense fallback={<div>Loading editor...</div>}>
{editorType === 'rich' ? <RichTextEditor /> : <MarkdownEditor />}
</Suspense>
</div>
)
}4.3 Modal/Dialog分割
typescript
// src/components/UserProfile.tsx
import { lazy, Suspense, useState } from 'react'
const EditProfileModal = lazy(() => import('./EditProfileModal'))
export function UserProfile() {
const [showModal, setShowModal] = useState(false)
return (
<div>
<button onClick={() => setShowModal(true)}>
Edit Profile
</button>
{showModal && (
<Suspense fallback={<div>Loading...</div>}>
<EditProfileModal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
)
}第五部分:库和vendor分割
5.1 Vite分包配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// React生态
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
// UI库
'ui-vendor': ['@mui/material', '@mui/icons-material'],
// 工具库
'utils-vendor': ['lodash-es', 'date-fns', 'axios'],
// 图表库
'chart-vendor': ['chart.js', 'react-chartjs-2'],
}
}
}
}
})5.2 自动分包策略
typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// node_modules 分包
if (id.includes('node_modules')) {
// React 相关
if (id.includes('react') || id.includes('react-dom')) {
return 'react'
}
// UI 库
if (id.includes('@mui')) {
return 'mui'
}
// 图表
if (id.includes('chart') || id.includes('echarts')) {
return 'charts'
}
// 其他第三方库
return 'vendor'
}
// 按目录分包
if (id.includes('/src/components/')) {
return 'components'
}
if (id.includes('/src/utils/')) {
return 'utils'
}
}
}
}
}
})5.3 Webpack分包配置
javascript
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// React
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react',
priority: 20
},
// UI库
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
priority: 15
},
// 图表
charts: {
test: /[\\/]node_modules[\\/](chart\.js|echarts)[\\/]/,
name: 'charts',
priority: 15
},
// 其他vendor
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
// 公共代码
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}第六部分:分割优化策略
6.1 分包大小控制
typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
const packageName = id.split('node_modules/')[1].split('/')[0]
// 大型库单独分包
if (['@mui/material', 'antd', 'echarts'].includes(packageName)) {
return packageName.replace('@', '')
}
return 'vendor'
}
}
}
},
chunkSizeWarningLimit: 500, // 500KB警告
}
})6.2 首屏优化
typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// 首页不懒加载
import Home from './pages/Home'
// 其他页面懒加载
const About = lazy(() => import('./pages/About'))
const Products = lazy(() => import('./pages/Products'))
const Contact = lazy(() => import('./pages/Contact'))
function App() {
return (
<Routes>
{/* 首页直接加载 */}
<Route path="/" element={<Home />} />
{/* 其他页面懒加载 */}
<Route
path="/about"
element={
<Suspense fallback={<div>Loading...</div>}>
<About />
</Suspense>
}
/>
<Route
path="/products"
element={
<Suspense fallback={<div>Loading...</div>}>
<Products />
</Suspense>
}
/>
<Route
path="/contact"
element={
<Suspense fallback={<div>Loading...</div>}>
<Contact />
</Suspense>
}
/>
</Routes>
)
}6.3 关键路径优化
typescript
// src/utils/critical.ts
// 关键资源不分割
export { default as Header } from '@/components/Header'
export { default as Footer } from '@/components/Footer'
export { default as Navigation } from '@/components/Navigation'
// 非关键资源懒加载
export const UserMenu = lazy(() => import('@/components/UserMenu'))
export const SearchModal = lazy(() => import('@/components/SearchModal'))
export const NotificationPanel = lazy(() => import('@/components/NotificationPanel'))第七部分:加载状态优化
7.1 加载占位符
typescript
// src/components/LoadingPlaceholder.tsx
export function LoadingPlaceholder({ type }: { type: 'page' | 'component' | 'modal' }) {
if (type === 'page') {
return (
<div className="page-skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
)
}
if (type === 'component') {
return <div className="component-skeleton" />
}
return <div className="modal-skeleton" />
}typescript
// 使用
const Dashboard = lazy(() => import('./pages/Dashboard'))
function App() {
return (
<Suspense fallback={<LoadingPlaceholder type="page" />}>
<Dashboard />
</Suspense>
)
}7.2 渐进式加载
typescript
// src/components/ProgressiveImage.tsx
import { useState, useEffect } from 'react'
export function ProgressiveImage({ src, placeholder }: { src: string, placeholder: string }) {
const [currentSrc, setCurrentSrc] = useState(placeholder)
useEffect(() => {
const img = new Image()
img.src = src
img.onload = () => setCurrentSrc(src)
}, [src])
return (
<img
src={currentSrc}
className={currentSrc === placeholder ? 'loading' : 'loaded'}
/>
)
}7.3 错误边界
typescript
// src/components/LazyErrorBoundary.tsx
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
}
export class LazyErrorBoundary extends Component<Props, State> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error) {
console.error('Lazy loading error:', error)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div>
<h2>加载失败</h2>
<button onClick={() => window.location.reload()}>
重新加载
</button>
</div>
)
}
return this.props.children
}
}typescript
// 使用
<LazyErrorBoundary>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</LazyErrorBoundary>第八部分:性能监控
8.1 加载性能追踪
typescript
// src/utils/performance.ts
export function trackChunkLoad(chunkName: string) {
const startTime = performance.now()
return () => {
const endTime = performance.now()
const loadTime = endTime - startTime
// 发送到分析服务
analytics.track('chunk_load', {
chunk: chunkName,
loadTime,
})
console.log(`Chunk ${chunkName} loaded in ${loadTime}ms`)
}
}typescript
// 使用
const Dashboard = lazy(() => {
const track = trackChunkLoad('dashboard')
return import('./pages/Dashboard').finally(track)
})8.2 Bundle分析
bash
# 安装分析工具
npm install -D rollup-plugin-visualizertypescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
]
})8.3 性能指标
typescript
// src/utils/metrics.ts
export function measureBundleSize() {
if ('performance' in window) {
const resources = performance.getEntriesByType('resource')
const jsResources = resources.filter(r => r.name.endsWith('.js'))
const totalSize = jsResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
console.log('Total JS Size:', (totalSize / 1024).toFixed(2), 'KB')
jsResources.forEach(r => {
console.log(`${r.name}: ${(r.transferSize / 1024).toFixed(2)} KB`)
})
}
}第九部分:完整示例
9.1 完整应用示例
typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { LazyErrorBoundary } from './components/LazyErrorBoundary'
import { LoadingPlaceholder } from './components/LoadingPlaceholder'
// 关键组件直接导入
import Header from './components/Header'
import Footer from './components/Footer'
// 页面懒加载
const Home = lazy(() => import('./pages/Home'))
const Products = lazy(() => import('./pages/Products'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Cart = lazy(() => import('./pages/Cart'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Profile = lazy(() => import('./pages/Profile'))
const Admin = lazy(() => import('./pages/Admin'))
function App() {
return (
<BrowserRouter>
<div className="app">
<Header />
<main>
<LazyErrorBoundary>
<Suspense fallback={<LoadingPlaceholder type="page" />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/profile" element={<Profile />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Suspense>
</LazyErrorBoundary>
</main>
<Footer />
</div>
</BrowserRouter>
)
}
export default App9.2 Vite完整配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({
open: true,
gzipSize: true,
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['@mui/material'],
'utils-vendor': ['lodash-es', 'axios'],
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
}
},
chunkSizeWarningLimit: 500,
}
})9.3 测试和验证
typescript
// 代码分割效果测试
describe('Code Splitting', () => {
it('should load chunks on demand', async () => {
const { getByText } = render(<App />);
// 初始不加载Dashboard
expect(window.loadedChunks).not.toContain('dashboard');
// 点击导航
fireEvent.click(getByText('Dashboard'));
// 等待加载
await waitFor(() => {
expect(window.loadedChunks).toContain('dashboard');
});
});
it('should show loading state', async () => {
const { getByText } = render(<App />);
fireEvent.click(getByText('Dashboard'));
// 显示loading
expect(getByText('Loading...')).toBeInTheDocument();
// 加载完成
await waitFor(() => {
expect(getByText('Dashboard Content')).toBeInTheDocument();
});
});
});第十部分:高级优化技巧
10.1 预加载优先级
typescript
// 根据用户行为预加载
function useSmartPreload() {
const location = useLocation();
const [userBehavior, setUserBehavior] = useState({
visitedRoutes: [],
clickPatterns: []
});
useEffect(() => {
// 分析用户行为
const patterns = analyzeUserBehavior(userBehavior);
// 预加载可能访问的路由
patterns.likelyNextRoutes.forEach(route => {
const component = routeComponents[route];
if (component?.preload) {
component.preload();
}
});
}, [userBehavior]);
return userBehavior;
}
// 机器学习预测
function predictNextRoute(currentRoute, history) {
const patterns = {
'/': ['/products', '/about'],
'/products': ['/products/:id', '/cart'],
'/products/:id': ['/cart', '/products'],
'/cart': ['/checkout']
};
return patterns[currentRoute] || [];
}10.2 动态chunk大小控制
typescript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 根据模块大小决定分包策略
const moduleInfo = this.getModuleInfo(id);
if (moduleInfo && moduleInfo.importedBindings) {
const bindings = Object.keys(moduleInfo.importedBindings);
// 大模块单独分包
if (bindings.length > 50) {
return `large-${id.split('/').pop()}`;
}
}
// 默认分包逻辑
if (id.includes('node_modules')) {
return 'vendor';
}
},
// 限制chunk大小
chunkSizeWarningLimit: 500,
// 最小chunk大小
minSize: 20000,
}
}
}
});10.3 Critical CSS提取
bash
npm install -D critterstypescript
// vite.config.ts
import { critters } from 'vite-plugin-critters';
export default defineConfig({
plugins: [
critters({
// 内联关键CSS
inlineFonts: true,
preload: 'swap',
pruneSource: true
})
]
});
// 生成结果
// index.html包含内联的关键CSS
// 非关键CSS延迟加载10.4 Module Federation
typescript
// Webpack Module Federation
// app1/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Header': './src/components/Header'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// app2/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app2',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// 使用远程组件
const RemoteButton = lazy(() => import('app1/Button'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RemoteButton />
</Suspense>
);
}第十一部分:性能监控和分析
11.1 Chunk加载监控
typescript
// 监控chunk加载性能
class ChunkLoadMonitor {
private metrics: Map<string, number> = new Map();
trackChunkLoad(chunkName: string, startTime: number) {
return () => {
const loadTime = performance.now() - startTime;
this.metrics.set(chunkName, loadTime);
// 发送到分析服务
analytics.track('chunk_load', {
chunk: chunkName,
loadTime,
url: window.location.href
});
// 性能告警
if (loadTime > 3000) {
console.warn(`Slow chunk load: ${chunkName} took ${loadTime}ms`);
}
};
}
getMetrics() {
const chunks = Array.from(this.metrics.entries());
return {
total: chunks.length,
average: chunks.reduce((sum, [, time]) => sum + time, 0) / chunks.length,
max: Math.max(...chunks.map(([, time]) => time)),
min: Math.min(...chunks.map(([, time]) => time)),
details: Object.fromEntries(chunks)
};
}
}
const monitor = new ChunkLoadMonitor();
// 使用
const Dashboard = lazy(() => {
const track = monitor.trackChunkLoad('dashboard', performance.now());
return import('./Dashboard').finally(track);
});11.2 Bundle分析报告
bash
# 生成分析报告
npm run build -- --analyze
# 或使用rollup-plugin-visualizer
npm install -D rollup-plugin-visualizertypescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
template: 'treemap' // sunburst, treemap, network
})
]
});
// 生成的报告显示:
// - 每个chunk的大小
// - 模块依赖关系
// - 压缩后大小
// - 优化建议11.3 性能指标收集
typescript
// 收集加载性能指标
function collectLoadingMetrics() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'resource') {
const resource = entry as PerformanceResourceTiming;
// 只关注chunk文件
if (resource.name.includes('/js/') || resource.name.includes('.chunk.')) {
const metrics = {
name: resource.name,
duration: resource.duration,
transferSize: resource.transferSize,
encodedBodySize: resource.encodedBodySize,
decodedBodySize: resource.decodedBodySize,
compressionRatio: resource.encodedBodySize
? (1 - resource.transferSize / resource.encodedBodySize) * 100
: 0
};
console.log('Chunk loaded:', metrics);
// 发送到分析服务
sendMetrics(metrics);
}
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
}
// 页面加载时执行
window.addEventListener('load', collectLoadingMetrics);第十二部分:错误处理和重试
12.1 Chunk加载失败处理
typescript
// 完整的错误处理方案
function lazyWithErrorHandling(
importFn: () => Promise<any>,
componentName: string
) {
return lazy(async () => {
try {
return await importFn();
} catch (error) {
console.error(`Failed to load ${componentName}:`, error);
// 检查是否是网络错误
if (error instanceof Error && error.message.includes('Failed to fetch')) {
// 显示重试UI
return {
default: () => (
<div className="chunk-error">
<h3>加载失败</h3>
<p>组件 {componentName} 加载失败</p>
<button onClick={() => window.location.reload()}>
重新加载页面
</button>
</div>
)
};
}
throw error;
}
});
}
// 使用
const Dashboard = lazyWithErrorHandling(
() => import('./Dashboard'),
'Dashboard'
);12.2 自动重试机制
typescript
// 带指数退避的重试
function lazyWithRetry(
importFn: () => Promise<any>,
options = { maxRetries: 3, retryDelay: 1000 }
) {
return lazy(() => {
return new Promise((resolve, reject) => {
let retries = 0;
const attemptLoad = () => {
importFn()
.then(resolve)
.catch((error) => {
if (retries < options.maxRetries) {
retries++;
const delay = options.retryDelay * Math.pow(2, retries - 1);
console.log(`Retry ${retries}/${options.maxRetries} in ${delay}ms`);
setTimeout(attemptLoad, delay);
} else {
console.error('Max retries reached');
reject(error);
}
});
};
attemptLoad();
});
});
}
// 使用
const Dashboard = lazyWithRetry(
() => import('./Dashboard'),
{ maxRetries: 3, retryDelay: 1000 }
);12.3 降级策略
typescript
// Chunk加载失败时的降级方案
function lazyWithFallback(
importFn: () => Promise<any>,
FallbackComponent: React.ComponentType
) {
return lazy(async () => {
try {
return await importFn();
} catch (error) {
console.error('Chunk load failed, using fallback:', error);
return {
default: FallbackComponent
};
}
});
}
// 简单降级组件
const SimpleDashboard = () => (
<div>
<h2>Dashboard (简化版)</h2>
<p>完整版本加载失败,这是简化版本</p>
</div>
);
// 使用
const Dashboard = lazyWithFallback(
() => import('./Dashboard'),
SimpleDashboard
);第十三部分:实战优化案例
13.1 电商网站优化
typescript
// 优化前: 单个bundle 2.5MB
// src/App.tsx
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import UserProfile from './pages/UserProfile';
import OrderHistory from './pages/OrderHistory';
// Bundle分析:
// main.js: 2.5MB (太大!)
// 优化后: 多个chunks
// src/App.tsx
import { lazy, Suspense } from 'react';
// 关键组件直接导入
import Header from './components/Header';
import Footer from './components/Footer';
// 页面懒加载
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import(
/* webpackChunkName: "products" */
/* webpackPrefetch: true */
'./pages/Products'
));
const ProductDetail = lazy(() => import(
/* webpackChunkName: "product-detail" */
'./pages/ProductDetail'
));
const Cart = lazy(() => import(
/* webpackChunkName: "cart" */
/* webpackPrefetch: true */
'./pages/Cart'
));
const Checkout = lazy(() => import(
/* webpackChunkName: "checkout" */
'./pages/Checkout'
));
const UserProfile = lazy(() => import(
/* webpackChunkName: "user" */
'./pages/UserProfile'
));
const OrderHistory = lazy(() => import(
/* webpackChunkName: "orders" */
'./pages/OrderHistory'
));
// Bundle结果:
// main.js: 150KB (↓94%)
// products.chunk.js: 200KB
// product-detail.chunk.js: 150KB
// cart.chunk.js: 100KB
// checkout.chunk.js: 180KB
// user.chunk.js: 120KB
// orders.chunk.js: 140KB
// vendor.chunk.js: 800KB
// 性能提升:
// 首次加载: 2.5MB -> 950KB (↓62%)
// FCP: 4.5s -> 1.8s (↓60%)
// TTI: 6.2s -> 2.5s (↓60%)13.2 SaaS后台系统优化
typescript
// 后台系统特点:
// - 功能模块多
// - 图表库大
// - 编辑器大
// - 不是所有用户都用所有功能
// 优化策略
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 图表库单独打包
if (id.includes('echarts') || id.includes('chart.js')) {
return 'charts';
}
// 编辑器单独打包
if (id.includes('monaco-editor') || id.includes('codemirror')) {
return 'editor';
}
// 表格库单独打包
if (id.includes('ag-grid') || id.includes('react-table')) {
return 'tables';
}
// UI库
if (id.includes('@mui') || id.includes('antd')) {
return 'ui';
}
// 按功能模块分包
if (id.includes('/src/features/dashboard')) {
return 'dashboard';
}
if (id.includes('/src/features/users')) {
return 'users';
}
if (id.includes('/src/features/analytics')) {
return 'analytics';
}
// 公共代码
if (id.includes('/src/components') || id.includes('/src/utils')) {
return 'common';
}
return 'vendor';
}
}
}
}
});
// 结果:
// 初始加载: 只加载必需的chunks
// 访问功能: 按需加载对应chunk
// 总体积不变,但初始加载减少80%13.3 移动端优化
typescript
// 移动端特殊考虑
const mobileOptimization = {
更激进的分割: `
// 移动端网络慢,分割更细
const Header = lazy(() => import('./Header'));
const Footer = lazy(() => import('./Footer'));
const Sidebar = lazy(() => import('./Sidebar'));
// 甚至大组件内部也分割
const ProductImage = lazy(() => import('./ProductImage'));
const ProductInfo = lazy(() => import('./ProductInfo'));
const ProductReviews = lazy(() => import('./ProductReviews'));
`,
预加载策略: `
// 在WiFi下预加载
if (navigator.connection?.effectiveType === '4g') {
// 预加载常用chunks
preloadChunks(['products', 'cart']);
}
`,
渐进式加载: `
// 先加载关键内容
<Suspense fallback={<Skeleton />}>
<ProductImage /> {/* 优先 */}
<ProductInfo /> {/* 优先 */}
</Suspense>
// 延迟加载次要内容
<Suspense fallback={null}>
<ProductReviews /> {/* 延迟 */}
</Suspense>
`
};第十四部分:测试和CI/CD集成
14.1 性能回归测试
typescript
// tests/performance.test.ts
import { analyzeBundle } from './utils/bundleAnalyzer';
describe('Bundle Performance', () => {
it('should not exceed size limits', async () => {
const analysis = await analyzeBundle('dist');
// 检查初始bundle大小
expect(analysis.initialBundle).toBeLessThan(200 * 1024); // 200KB
// 检查最大chunk大小
const maxChunk = Math.max(...analysis.chunks.map(c => c.size));
expect(maxChunk).toBeLessThan(500 * 1024); // 500KB
// 检查总大小
expect(analysis.totalSize).toBeLessThan(3 * 1024 * 1024); // 3MB
});
it('should have proper code splitting', () => {
const analysis = await analyzeBundle('dist');
// 检查chunk数量
expect(analysis.chunks.length).toBeGreaterThan(3);
// 检查vendor分离
const hasVendorChunk = analysis.chunks.some(c => c.name.includes('vendor'));
expect(hasVendorChunk).toBe(true);
// 检查路由分割
const routeChunks = analysis.chunks.filter(c =>
c.name.includes('route') || c.name.includes('page')
);
expect(routeChunks.length).toBeGreaterThan(2);
});
});14.2 CI/CD集成
yaml
# .github/workflows/bundle-check.yml
name: Bundle Size Check
on: [pull_request]
jobs:
bundle-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Analyze bundle
run: npm run analyze
- name: Check bundle size
run: |
node scripts/check-bundle-size.js
- name: Comment PR
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const stats = JSON.parse(fs.readFileSync('dist/stats.json'));
const comment = `
## Bundle Size Report
| Chunk | Size | Gzipped |
|-------|------|---------|
${stats.chunks.map(c =>
`| ${c.name} | ${c.size}KB | ${c.gzipped}KB |`
).join('\n')}
Total: ${stats.total}KB
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});14.3 性能预算
javascript
// performance-budget.json
{
"bundles": [
{
"name": "main",
"maxSize": "200KB",
"maxSizeGzip": "70KB"
},
{
"name": "vendor",
"maxSize": "800KB",
"maxSizeGzip": "250KB"
},
{
"name": "*.chunk",
"maxSize": "500KB",
"maxSizeGzip": "150KB"
}
],
"total": {
"maxSize": "3MB",
"maxSizeGzip": "1MB"
}
}
// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');
const budget = require('../performance-budget.json');
function checkBundleSize() {
const distPath = path.join(__dirname, '../dist');
const files = fs.readdirSync(distPath);
const violations = [];
files.forEach(file => {
const filePath = path.join(distPath, file);
const stats = fs.statSync(filePath);
const sizeKB = stats.size / 1024;
const budgetRule = budget.bundles.find(b =>
file.match(new RegExp(b.name))
);
if (budgetRule) {
const maxSizeKB = parseInt(budgetRule.maxSize);
if (sizeKB > maxSizeKB) {
violations.push({
file,
actual: sizeKB,
budget: maxSizeKB,
over: sizeKB - maxSizeKB
});
}
}
});
if (violations.length > 0) {
console.error('Bundle size budget exceeded:');
violations.forEach(v => {
console.error(`${v.file}: ${v.actual}KB > ${v.budget}KB (+${v.over}KB)`);
});
process.exit(1);
}
console.log('✓ All bundles within budget');
}
checkBundleSize();总结
本章全面介绍了代码分割策略:
- 基础概念 - 理解代码分割的重要性和原理
- 动态导入 - 使用import()实现按需加载
- 路由分割 - 按页面路由拆分代码
- 组件分割 - 组件级别的懒加载优化
- 库分割 - 第三方库的合理分包
- 优化策略 - 分包大小控制和预加载
- 加载状态 - 优化用户体验的Loading处理
- 性能监控 - 追踪和分析chunk加载性能
- 高级技巧 - 智能预加载、动态优化
- 错误处理 - 完善的失败重试机制
- 实战案例 - 真实项目优化经验
- CI/CD集成 - 自动化性能检查
合理的代码分割是提升应用性能的关键技术,通过系统化的分割策略可以显著改善用户体验。