Appearance
第7章:懒加载 - 代码分割与性能优化
状态: ✅ 已完成 难度: ⭐⭐⭐⭐ 预计时间: 2.5小时 GitHub 演示: 查看源码 →
学习目标
- 理解代码分割的原理和优势
- 实现路由级别的懒加载
- 掌握加载状态和错误处理
- 实现预加载和缓存策略
核心概念
1. 代码分割
代码分割是将应用代码拆分成多个小块,按需加载的技术:
javascript
// 传统方式 - 所有代码打包在一起
import Home from './pages/Home';
import About from './pages/About';
// 懒加载方式 - 代码分割成独立块
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));2. React.lazy 和 Suspense
jsx
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}3. 预加载策略
- 空闲时预加载: 使用
requestIdleCallback - 鼠标悬停预加载: 用户交互时预加载
- 可见性预加载: 元素进入视口时预加载
实现步骤
步骤1:扩展类型定义
typescript
// src/types.ts
export interface LazyRouteConfig extends RouteConfig {
lazy: () => Promise<{ default: React.ComponentType<any> }>;
preload?: () => Promise<void>; // 预加载函数
loading?: React.ComponentType<{ error?: Error; retry: () => void }>;
error?: React.ComponentType<{ error: Error; retry: () => void }>;
}
export interface LazyComponentState {
Component: React.ComponentType<any> | null;
loading: boolean;
error: Error | null;
preloadPromise: Promise<void> | null;
}
export interface PreloadStrategy {
type: 'hover' | 'visible' | 'idle' | 'manual';
delay?: number;
threshold?: number;
}步骤2:实现 LazyRoute 组件
typescript
// src/components/LazyRoute.tsx
import React, { Suspense, lazy, ComponentType } from 'react';
import type { LazyRouteConfig } from '../types';
interface LazyRouteProps {
route: LazyRouteConfig;
params: Record<string, string>;
}
function DefaultLoadingComponent() {
return <div className="lazy-loading">加载中...</div>;
}
function DefaultErrorComponent({ error, retry }: { error: Error; retry: () => void }) {
return (
<div className="lazy-error">
<h3>加载失败</h3>
<p>{error.message}</p>
<button onClick={retry}>重试</button>
</div>
);
}
export function LazyRoute({ route, params }: LazyRouteProps) {
const LazyComponent = React.lazy(() => {
return route.lazy().then(module => ({
default: () => {
const Component = module.default;
return <Component {...params} />;
}
}));
});
const LoadingComponent = route.loading || DefaultLoadingComponent;
const ErrorComponent = route.error || DefaultErrorComponent;
return (
<ErrorBoundary fallback={ErrorComponent}>
<Suspense fallback={<LoadingComponent />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
// 错误边界组件
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: ComponentType<{ error: Error; retry: () => void }> },
{ hasError: boolean; error: Error | null }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('LazyRoute loading error:', error, errorInfo);
}
retry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError && this.state.error) {
const FallbackComponent = this.props.fallback;
return <FallbackComponent error={this.state.error} retry={this.retry} />;
}
return this.props.children;
}
}步骤3:实现预加载管理器
typescript
// src/utils/preloadManager.ts
interface PreloadEntry {
promise: Promise<void>;
resolved: boolean;
error: Error | null;
}
class PreloadManager {
private cache = new Map<string, PreloadEntry>();
private observers = new Map<string, IntersectionObserver>();
// 预加载组件
async preload(lazyFn: () => Promise<{ default: any }>, key: string): Promise<void> {
// 检查缓存
const cached = this.cache.get(key);
if (cached?.resolved) {
if (cached.error) {
throw cached.error;
}
return;
}
// 创建新的预加载条目
const entry: PreloadEntry = {
promise: this.createPreloadPromise(lazyFn, key),
resolved: false,
error: null
};
this.cache.set(key, entry);
try {
await entry.promise;
entry.resolved = true;
} catch (error) {
entry.error = error as Error;
throw error;
}
}
private async createPreloadPromise(
lazyFn: () => Promise<{ default: any }>,
key: string
): Promise<void> {
try {
const module = await lazyFn();
// 预加载成功,组件已缓存
console.log(`Component ${key} preloaded successfully`);
} catch (error) {
console.error(`Failed to preload component ${key}:`, error);
throw error;
}
}
// 悬停预加载
setupHoverPreload(element: HTMLElement, lazyFn: () => Promise<{ default: any }>, key: string, delay = 200) {
let timeoutId: NodeJS.Timeout;
const handleMouseEnter = () => {
timeoutId = setTimeout(() => {
this.preload(lazyFn, key);
}, delay);
};
const handleMouseLeave = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
// 返回清理函数
return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}
// 可见性预加载
setupVisiblePreload(
element: HTMLElement,
lazyFn: () => Promise<{ default: any }>,
key: string,
threshold = 0.1
) {
// 清理已存在的观察者
if (this.observers.has(key)) {
this.observers.get(key)?.disconnect();
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.preload(lazyFn, key);
observer.unobserve(element);
this.observers.delete(key);
}
});
},
{ threshold }
);
observer.observe(element);
this.observers.set(key, observer);
// 返回清理函数
return () => {
observer.unobserve(element);
this.observers.delete(key);
};
}
// 空闲时预加载
setupIdlePreload(lazyFn: () => Promise<{ default: any }>, key: string) {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
this.preload(lazyFn, key);
});
} else {
// 降级到 setTimeout
setTimeout(() => {
this.preload(lazyFn, key);
}, 100);
}
}
// 清理缓存
clearCache(key?: string) {
if (key) {
this.cache.delete(key);
this.observers.get(key)?.disconnect();
this.observers.delete(key);
} else {
this.cache.clear();
this.observers.forEach(observer => observer.disconnect());
this.observers.clear();
}
}
// 获取缓存状态
getCacheStatus(key: string): 'pending' | 'resolved' | 'error' | 'not-found' {
const entry = this.cache.get(key);
if (!entry) return 'not-found';
if (entry.resolved) return 'resolved';
if (entry.error) return 'error';
return 'pending';
}
}
export const preloadManager = new PreloadManager();步骤4:实现智能 Link 组件
typescript
// src/components/SmartLink.tsx
import React, { useRef, useEffect } from 'react';
import { Link } from './Link';
import { preloadManager } from '../utils/preloadManager';
import type { LazyRouteConfig } from '../types';
interface SmartLinkProps {
to: string;
route?: LazyRouteConfig;
preloadStrategy?: PreloadStrategy;
children: React.ReactNode;
className?: string;
}
export function SmartLink({
to,
route,
preloadStrategy = { type: 'hover' },
children,
className
}: SmartLinkProps) {
const linkRef = useRef<HTMLAnchorElement>(null);
useEffect(() => {
if (!route || !linkRef.current) return;
const element = linkRef.current;
const cleanup: (() => void)[] = [];
switch (preloadStrategy.type) {
case 'hover':
cleanup.push(
preloadManager.setupHoverPreload(
element,
route.lazy,
route.path,
preloadStrategy.delay
)
);
break;
case 'visible':
cleanup.push(
preloadManager.setupVisiblePreload(
element,
route.lazy,
route.path,
preloadStrategy.threshold
)
);
break;
case 'idle':
preloadManager.setupIdlePreload(route.lazy, route.path);
break;
}
return () => {
cleanup.forEach(fn => fn());
};
}, [route, preloadStrategy]);
return (
<Link ref={linkRef} to={to} className={className}>
{children}
</Link>
);
}步骤5:更新路由匹配支持懒加载
typescript
// src/utils/routeMatcher.ts (更新版)
import type { LazyRouteConfig } from '../types';
export function matchRoutesAdvanced(
routes: RouteConfig[],
pathname: string,
basename = ''
): RouteMatch[] {
const allMatches: RouteMatch[] = [];
function findMatches(
routes: RouteConfig[],
pathname: string,
basePath = '',
params: Record<string, string> = {},
depth = 0
): void {
for (const route of routes) {
const match = matchPathPattern(route, pathname, route.caseSensitive);
if (match) {
const newParams = { ...params, ...match.params };
const matchedPath = joinPaths(basePath, route.path);
// 处理懒加载路由
if ('lazy' in route) {
const lazyRoute = route as LazyRouteConfig;
// 立即预加载
if (lazyRoute.preload) {
lazyRoute.preload();
} else {
preloadManager.preload(lazyRoute.lazy, lazyRoute.path).catch(() => {
// 预加载失败不影响正常导航
});
}
}
allMatches.push({
route,
pathname: matchedPath,
params: newParams
});
// 递归匹配子路由
if (route.children && match.remainingPath) {
findMatches(
route.children,
match.remainingPath,
matchedPath,
newParams,
depth + 1
);
}
}
}
}
findMatches(routes, pathname);
return selectBestMatches(allMatches);
}步骤6:创建懒加载辅助函数
typescript
// src/utils/lazyUtils.ts
import { lazy } from 'react';
import type { LazyRouteConfig } from '../types';
// 创建懒加载路由
export function createLazyRoute(
path: string,
lazyFn: () => Promise<{ default: React.ComponentType<any> }>,
options: Partial<Pick<LazyRouteConfig, 'loading' | 'error' | 'preload'>> = {}
): LazyRouteConfig {
const route: LazyRouteConfig = {
path,
lazy: lazyFn,
element: null as any, // 将在 LazyRoute 中处理
...options
};
// 创建预加载函数
if (!route.preload) {
route.preload = () => preloadManager.preload(lazyFn, path);
}
return route;
}
// 批量创建懒加载路由
export function createLazyRoutes(routesConfig: Array<{
path: string;
importPath: string;
componentName?: string;
loading?: React.ComponentType<any>;
error?: React.ComponentType<any>;
}>): LazyRouteConfig[] {
return routesConfig.map(config => {
const lazyFn = () => import(config.importPath).then(module => {
const Component = config.componentName
? module[config.componentName]
: module.default;
if (!Component) {
throw new Error(`Component ${config.componentName || 'default'} not found in ${config.importPath}`);
}
return { default: Component };
});
return createLazyRoute(config.path, lazyFn, {
loading: config.loading,
error: config.error
});
});
}
// 预加载多个路由
export async function preloadRoutes(routes: LazyRouteConfig[]) {
const preloadPromises = routes.map(route => {
if (route.preload) {
return route.preload().catch(error => {
console.warn(`Failed to preload route ${route.path}:`, error);
});
}
return Promise.resolve();
});
await Promise.allSettled(preloadPromises);
}完整示例
typescript
// App.tsx
import React, { lazy, Suspense } from 'react';
import {
RouterProvider,
Routes,
Route,
Link,
SmartLink,
preloadManager,
createLazyRoute
} from './mini-router';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Settings = lazy(() => import('./pages/Settings'));
// 自定义加载组件
const CustomLoading = () => (
<div className="custom-loading">
<div className="spinner"></div>
<p>正在加载页面...</p>
</div>
);
const CustomError = ({ error, retry }: { error: Error; retry: () => void }) => (
<div className="custom-error">
<h3>😞 页面加载失败</h3>
<p>{error.message}</p>
<button onClick={retry} className="retry-button">重试</button>
</div>
);
// 创建懒加载路由
const lazyRoutes = [
createLazyRoute('/about', () => import('./pages/About'), {
loading: CustomLoading,
error: CustomError
}),
createLazyRoute('/dashboard', () => import('./pages/Dashboard')),
createLazyRoute('/users/:id', () => import('./pages/UserProfile')),
createLazyRoute('/settings', () => import('./pages/Settings'))
];
// 布局组件
const Layout = () => (
<div className="layout">
<header>
<nav>
<SmartLink
to="/"
route={createLazyRoute('/', () => import('./pages/Home'))}
preloadStrategy={{ type: 'hover', delay: 100 }}
>
首页
</SmartLink>
<SmartLink
to="/about"
route={lazyRoutes[0]}
preloadStrategy={{ type: 'hover', delay: 150 }}
>
关于
</SmartLink>
<SmartLink
to="/dashboard"
route={lazyRoutes[1]}
preloadStrategy={{ type: 'visible', threshold: 0.5 }}
>
仪表板
</SmartLink>
<SmartLink
to="/users/123"
route={lazyRoutes[2]}
preloadStrategy={{ type: 'idle' }}
>
用户123
</SmartLink>
<Link to="/settings">设置</Link>
</nav>
</header>
<main>
<Routes />
</main>
</div>
);
// 预加载管理组件
const PreloadManager = () => {
const [preloadStatus, setPreloadStatus] = React.useState<Record<string, string>>({});
React.useEffect(() => {
// 监听预加载状态
const updateStatus = () => {
const status: Record<string, string> = {};
lazyRoutes.forEach(route => {
status[route.path] = preloadManager.getCacheStatus(route.path);
});
setPreloadStatus(status);
};
const interval = setInterval(updateStatus, 500);
updateStatus();
return () => clearInterval(interval);
}, []);
return (
<div className="preload-status">
<h4>预加载状态:</h4>
{Object.entries(preloadStatus).map(([path, status]) => (
<div key={path}>
{path}: <span className={`status-${status}`}>{status}</span>
</div>
))}
</div>
);
};
// 路由配置
const routes = [
{
path: '/',
element: <Layout />,
children: [
{
path: '',
element: (
<Suspense fallback={<CustomLoading />}>
<Home />
</Suspense>
)
},
...lazyRoutes.map(route => ({
...route,
element: <LazyRoute route={route} params={{}} />
})),
{
path: '*',
element: <div>404 - 页面未找到</div>
}
]
}
];
function App() {
// 预加载重要路由
React.useEffect(() => {
// 预加载首页和仪表板
preloadManager.preload(() => import('./pages/Home'), '/home');
preloadManager.preload(() => import('./pages/Dashboard'), '/dashboard');
}, []);
return (
<div className="app">
<RouterProvider routes={routes}>
<Routes />
</RouterProvider>
<PreloadManager />
</div>
);
}
export default App;样式示例
css
.custom-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.custom-error {
text-align: center;
padding: 2rem;
color: #dc3545;
}
.retry-button {
background-color: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
.retry-button:hover {
background-color: #0056b3;
}
.preload-status {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 1rem;
border-radius: 4px;
font-size: 0.8rem;
}
.status-pending { color: #ffc107; }
.status-resolved { color: #28a745; }
.status-error { color: #dc3545; }
.status-not-found { color: #6c757d; }高级用法示例
1. 带超时的懒加载
typescript
function createLazyRouteWithTimeout(
path: string,
lazyFn: () => Promise<{ default: React.ComponentType<any> }>,
timeout = 5000
): LazyRouteConfig {
return createLazyRoute(path, async () => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('加载超时')), timeout);
});
return Promise.race([lazyFn(), timeoutPromise]);
});
}2. 渐进式加载
typescript
function createProgressiveLoader(components: Array<{
path: string;
priority: number;
lazyFn: () => Promise<{ default: any }>;
}>) {
// 按优先级排序
const sorted = [...components].sort((a, b) => a.priority - b.priority);
// 依次加载
sorted.forEach((component, index) => {
const delay = index * 1000; // 每个组件延迟1秒
setTimeout(() => {
preloadManager.preload(component.lazyFn, component.path);
}, delay);
});
}3. 智能预加载策略
typescript
class SmartPreloader {
private userBehavior: UserBehavior = {
visitedPages: [],
commonPaths: [],
timeOnPages: {}
};
// 分析用户行为
analyzeBehavior(currentPath: string) {
// 预测用户可能访问的页面
const predictions = this.predictNextPages(currentPath);
// 预加载高概率页面
predictions.forEach(({ path, probability }) => {
if (probability > 0.7) {
preloadManager.preload(this.getLazyFn(path), path);
}
});
}
private predictNextPages(currentPath: string): Array<{ path: string; probability: number }> {
// 基于历史数据预测下一个页面
// 这里可以实现复杂的机器学习算法
return [
{ path: '/dashboard', probability: 0.8 },
{ path: '/profile', probability: 0.6 }
];
}
private getLazyFn(path: string) {
// 根据路径返回对应的懒加载函数
const lazyMap: Record<string, () => Promise<any>> = {
'/dashboard': () => import('./pages/Dashboard'),
'/profile': () => import('./pages/Profile')
};
return lazyMap[path];
}
}性能优化技巧
1. 代码分割策略
javascript
// 按路由分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// 按功能分割
const AdminDashboard = lazy(() => import('./features/admin/Dashboard'));
const UserDashboard = lazy(() => import('./features/user/Dashboard'));
// 按组件尺寸分割
const HeavyComponent = lazy(() => import(/* webpackChunkName: "heavy" */ './components/Heavy'));
const LightComponent = lazy(() => import(/* webpackChunkName: "light" */ './components/Light'));2. 预加载优化
typescript
// 关键路由立即预加载
const criticalRoutes = ['/dashboard', '/profile'];
criticalRoutes.forEach(path => {
preloadManager.preload(getLazyFn(path), path);
});
// 非关键路由延迟预加载
setTimeout(() => {
const nonCriticalRoutes = ['/settings', '/help'];
nonCriticalRoutes.forEach(path => {
preloadManager.preload(getLazyFn(path), path);
});
}, 3000);关键点解析
1. 懒加载原理
typescript
const LazyComponent = lazy(() => import('./Component'));
// 编译后会变成:
const LazyComponent = lazy(() =>
__webpack_require__.e(/* import() */ 0).then(__webpack_require__.bind(null, 1))
);2. Suspense 边界
jsx
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
// 当 LazyComponent 加载时,显示 Loading 组件
// 加载完成后,自动切换到 LazyComponent3. 错误处理
typescript
class ErrorBoundary extends Component {
componentDidCatch(error, errorInfo) {
// 记录错误
console.error('Lazy loading error:', error);
// 发送错误报告
reportError(error, errorInfo);
}
}练习题
- 基础练习: 实现带进度的加载指示器
- 进阶练习: 添加基于网络状况的自适应加载
- 挑战练习: 实现智能预加载算法
常见问题
Q: 懒加载会影响SEO吗?
A: 服务器端渲染(SSR)时,懒加载组件会在服务器上完整渲染,不影响SEO。客户端渲染时,搜索引擎可能无法抓取懒加载内容。
Q: 如何处理懒加载的样式?
A: 使用 CSS-in-JS 或确保样式文件不依赖于组件的懒加载,可以在主入口中提前加载关键样式。
下一步预告
下一章我们将实现 Link 状态管理,包括:
- NavLink 组件的实现
- useMatch 和 useResolvedPath Hook
- 路由状态同步
- 导航交互优化