Appearance
代码分割(lazy-import)
第一部分:代码分割基础
1.1 什么是代码分割
代码分割(Code Splitting)是将应用代码拆分成多个bundle的技术,按需加载所需代码,而不是一次性加载整个应用,从而减少初始加载时间,提升性能。
核心概念:
javascript
// 传统打包:单一bundle
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';
import ComponentC from './ComponentC';
function App() {
return (
<div>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
);
}
// 问题:bundle.js 包含所有代码,体积大
// 代码分割:按需加载
const ComponentA = lazy(() => import('./ComponentA'));
const ComponentB = lazy(() => import('./ComponentB'));
const ComponentC = lazy(() => import('./ComponentC'));
function App() {
return (
<Suspense fallback={<Loading />}>
<ComponentA />
<ComponentB />
<ComponentC />
</Suspense>
);
}
// 优势:拆分成多个小chunk,按需加载1.2 代码分割的优势
javascript
// 1. 性能提升
// - 减少初始bundle大小
// - 加快首屏加载速度
// - 降低Time to Interactive (TTI)
// - 优化Core Web Vitals
// 2. 用户体验
// - 更快的页面响应
// - 减少白屏时间
// - 流畅的交互体验
// - 节省用户带宽
// 3. 开发效率
// - 模块化开发
// - 独立部署
// - 更好的缓存策略
// - 易于维护
// 示例对比
// 传统方式
// main.js: 500KB (包含所有代码)
// 加载时间: 3-5秒
// 代码分割后
// main.js: 100KB (核心代码)
// chunk-1.js: 150KB (路由A)
// chunk-2.js: 150KB (路由B)
// chunk-3.js: 100KB (路由C)
// 初始加载时间: 1秒1.3 React.lazy基础
javascript
// React.lazy语法
const LazyComponent = React.lazy(() => import('./Component'));
// 基本使用
import { lazy, Suspense } from 'react';
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<About />
</Suspense>
);
}
// 多个lazy组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
);
}
// 命名导出处理
// Component.js
export function MyComponent() {
return <div>My Component</div>;
}
// App.js
const MyComponent = lazy(() =>
import('./Component').then(module => ({
default: module.MyComponent
}))
);
// 错误处理
const SafeComponent = lazy(() =>
import('./Component').catch(error => {
console.error('Failed to load component:', error);
return { default: () => <div>Failed to load</div> };
})
);1.4 动态import()
javascript
// 基础动态导入
button.addEventListener('click', async () => {
const module = await import('./module.js');
module.default();
});
// React中使用
function Component() {
const [Module, setModule] = useState(null);
const loadModule = async () => {
const { default: LoadedModule } = await import('./Module');
setModule(() => LoadedModule);
};
return (
<div>
<button onClick={loadModule}>Load Module</button>
{Module && <Module />}
</div>
);
}
// 条件导入
async function loadEditor(type) {
if (type === 'rich') {
return import('./RichTextEditor');
} else if (type === 'markdown') {
return import('./MarkdownEditor');
} else {
return import('./PlainTextEditor');
}
}
// 并行导入
async function loadMultiple() {
const [moduleA, moduleB, moduleC] = await Promise.all([
import('./ModuleA'),
import('./ModuleB'),
import('./ModuleC')
]);
return {
A: moduleA.default,
B: moduleB.default,
C: moduleC.default
};
}第二部分:分割策略
2.1 路由级分割
javascript
// React Router + lazy
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// 嵌套路由分割
const UserRoutes = lazy(() => import('./routes/UserRoutes'));
const AdminRoutes = lazy(() => import('./routes/AdminRoutes'));
function App() {
return (
<Suspense fallback={<RouteLoader />}>
<Routes>
<Route path="/user/*" element={<UserRoutes />} />
<Route path="/admin/*" element={<AdminRoutes />} />
</Routes>
</Suspense>
);
}
// 每个路由独立fallback
function App() {
return (
<Routes>
<Route
path="/"
element={
<Suspense fallback={<HomeLoader />}>
<Home />
</Suspense>
}
/>
<Route
path="/dashboard"
element={
<Suspense fallback={<DashboardLoader />}>
<Dashboard />
</Suspense>
}
/>
</Routes>
);
}
// 路由预加载
const routes = [
{ path: '/', component: lazy(() => import('./Home')) },
{ path: '/about', component: lazy(() => import('./About')) }
];
// 添加preload方法
function lazyWithPreload(importFunc) {
const LazyComponent = lazy(importFunc);
LazyComponent.preload = importFunc;
return LazyComponent;
}
const About = lazyWithPreload(() => import('./About'));
// 预加载使用
function NavLink({ to }) {
const handleMouseEnter = () => {
About.preload();
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
About
</Link>
);
}2.2 组件级分割
javascript
// 按功能分割
const Header = lazy(() => import('./components/Header'));
const Footer = lazy(() => import('./components/Footer'));
const Sidebar = lazy(() => import('./components/Sidebar'));
function Layout() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<main>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Content />
</main>
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</div>
);
}
// 重量级组件分割
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));
const Editor = lazy(() => import('./components/Editor'));
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<ChartSkeleton />}>
<Chart data={chartData} />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable data={tableData} />
</Suspense>
<Suspense fallback={<EditorSkeleton />}>
<Editor content={content} />
</Suspense>
</div>
);
}
// 模态框分割
const Modal = lazy(() => import('./components/Modal'));
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>
Open Modal
</button>
{showModal && (
<Suspense fallback={<ModalLoader />}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
}
// 条件加载
function ConditionalSplit({ userType }) {
const AdminPanel = lazy(() => import('./AdminPanel'));
const UserPanel = lazy(() => import('./UserPanel'));
return (
<Suspense fallback={<PanelLoader />}>
{userType === 'admin' ? <AdminPanel /> : <UserPanel />}
</Suspense>
);
}2.3 第三方库分割
javascript
// 重量级库按需加载
// Moment.js示例
function DateComponent() {
const [moment, setMoment] = useState(null);
useEffect(() => {
import('moment').then(mod => {
setMoment(() => mod.default);
});
}, []);
if (!moment) return <div>Loading...</div>;
return <div>{moment().format('YYYY-MM-DD')}</div>;
}
// Chart.js懒加载
const ChartComponent = lazy(() =>
import('react-chartjs-2').then(module => ({
default: module.Line
}))
);
function Analytics() {
return (
<Suspense fallback={<ChartSkeleton />}>
<ChartComponent data={chartData} />
</Suspense>
);
}
// Lodash函数级导入
button.addEventListener('click', async () => {
const { debounce } = await import('lodash-es');
const debouncedFn = debounce(myFunction, 300);
debouncedFn();
});
// 图标库按需加载
const iconModules = {
home: () => import('@icons/home'),
user: () => import('@icons/user'),
settings: () => import('@icons/settings')
};
function Icon({ name }) {
const [IconComponent, setIconComponent] = useState(null);
useEffect(() => {
iconModules[name]().then(mod => {
setIconComponent(() => mod.default);
});
}, [name]);
if (!IconComponent) return null;
return <IconComponent />;
}
// 国际化文件分割
async function loadLocale(locale) {
const messages = await import(`./locales/${locale}.json`);
return messages.default;
}
function I18nProvider({ locale, children }) {
const [messages, setMessages] = useState(null);
useEffect(() => {
loadLocale(locale).then(setMessages);
}, [locale]);
if (!messages) return <Loading />;
return (
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
);
}2.4 Webpack魔法注释
javascript
// webpackChunkName: 命名chunk
const About = lazy(() =>
import(/* webpackChunkName: "about" */ './pages/About')
);
// 生成: about.chunk.js
// webpackPrefetch: 预获取
const Dashboard = lazy(() =>
import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./pages/Dashboard'
)
);
// 浏览器空闲时预获取
// webpackPreload: 预加载
const Critical = lazy(() =>
import(
/* webpackChunkName: "critical" */
/* webpackPreload: true */
'./components/Critical'
)
);
// 父chunk加载时并行预加载
// webpackMode: 导入模式
const DynamicComponent = lazy(() =>
import(
/* webpackMode: "lazy" */
`./components/${componentName}`
)
);
// 组合使用
const AdminPanel = lazy(() =>
import(
/* webpackChunkName: "admin-panel" */
/* webpackPrefetch: true */
/* webpackPreload: true */
'./pages/AdminPanel'
)
);
// 按组分割
const UserRoutes = lazy(() =>
import(/* webpackChunkName: "user-routes" */ './routes/UserRoutes')
);
const AdminRoutes = lazy(() =>
import(/* webpackChunkName: "admin-routes" */ './routes/AdminRoutes')
);
const PublicRoutes = lazy(() =>
import(/* webpackChunkName: "public-routes" */ './routes/PublicRoutes')
);第三部分:高级技巧
3.1 预加载策略
javascript
// 路由预加载
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const routeComponents = {
'/': () => import('./pages/Home'),
'/about': () => import('./pages/About'),
'/dashboard': () => import('./pages/Dashboard')
};
function RoutePreloader() {
const location = useLocation();
useEffect(() => {
// 预加载相关路由
const relatedRoutes = getRelatedRoutes(location.pathname);
relatedRoutes.forEach(route => {
if (routeComponents[route]) {
routeComponents[route]();
}
});
}, [location]);
return null;
}
function getRelatedRoutes(currentPath) {
const routeMap = {
'/': ['/about', '/contact'],
'/products': ['/products/details', '/cart'],
'/dashboard': ['/profile', '/settings']
};
return routeMap[currentPath] || [];
}
// 交互预加载
function NavLink({ to, children }) {
const handleMouseEnter = () => {
if (routeComponents[to]) {
routeComponents[to]();
}
};
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}
// 智能预加载
function SmartPreloader() {
useEffect(() => {
// 网络空闲时预加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
Object.values(routeComponents).forEach(loader => {
loader();
});
});
}
}, []);
return null;
}
// 基于用户行为预加载
function BehaviorBasedPreload() {
const [userIntent, setUserIntent] = useState(null);
useEffect(() => {
const handleMouseMove = (e) => {
// 检测鼠标移向导航区域
if (e.clientY < 100) {
setUserIntent('navigation');
}
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
useEffect(() => {
if (userIntent === 'navigation') {
// 预加载所有路由
Object.values(routeComponents).forEach(loader => loader());
}
}, [userIntent]);
return null;
}3.2 错误处理
javascript
// 错误边界 + Suspense
class LazyErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
if (error.name === 'ChunkLoadError') {
console.error('Chunk load failed, reloading page...');
window.location.reload();
}
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>加载失败</h2>
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<LazyErrorBoundary>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</LazyErrorBoundary>
);
}
// 重试机制
function lazyWithRetry(importFunc, retries = 3) {
return lazy(() => {
return new Promise((resolve, reject) => {
const attempt = (retriesLeft) => {
importFunc()
.then(resolve)
.catch((error) => {
if (retriesLeft === 0) {
reject(error);
return;
}
setTimeout(() => {
attempt(retriesLeft - 1);
}, 1000);
});
};
attempt(retries);
});
});
}
// 使用
const RobustComponent = lazyWithRetry(
() => import('./Component'),
3
);
// 降级处理
function LazyWithFallback() {
const [Component, setComponent] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
import('./Component')
.then(mod => setComponent(() => mod.default))
.catch(err => {
setError(err);
// 降级到简单组件
setComponent(() => SimpleFallbackComponent);
});
}, []);
if (error) {
console.error('Failed to load component:', error);
}
if (!Component) return <Loading />;
return <Component />;
}3.3 性能优化
javascript
// bundle分析
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};
// 动态chunk大小优化
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
// 代码压缩
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ['console.log']
}
}
})
]
}
};
// 缓存策略
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};3.4 SSR代码分割
javascript
// loadable-components (SSR支持)
import loadable from '@loadable/component';
const OtherComponent = loadable(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent fallback={<div>Loading...</div>} />
</div>
);
}
// 服务端
import { ChunkExtractor } from '@loadable/server';
app.get('*', (req, res) => {
const extractor = new ChunkExtractor({
statsFile: path.resolve('build/loadable-stats.json')
});
const jsx = extractor.collectChunks(<App />);
const html = renderToString(jsx);
res.send(`
<!DOCTYPE html>
<html>
<head>
${extractor.getStyleTags()}
${extractor.getLinkTags()}
</head>
<body>
<div id="root">${html}</div>
${extractor.getScriptTags()}
</body>
</html>
`);
});
// Next.js动态导入
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('./Component'), {
loading: () => <p>Loading...</p>,
ssr: true
});
function Page() {
return (
<div>
<DynamicComponent />
</div>
);
}
// 禁用SSR
const NoSSR = dynamic(() => import('./NoSSR'), {
ssr: false
});第四部分:实战案例
4.1 大型应用分割
javascript
// 分层分割策略
// 1. 路由层
const routes = {
public: lazy(() => import('./routes/PublicRoutes')),
user: lazy(() => import('./routes/UserRoutes')),
admin: lazy(() => import('./routes/AdminRoutes'))
};
// 2. 功能模块层
const features = {
dashboard: lazy(() => import('./features/Dashboard')),
analytics: lazy(() => import('./features/Analytics')),
settings: lazy(() => import('./features/Settings'))
};
// 3. 组件层
const components = {
chart: lazy(() => import('./components/Chart')),
table: lazy(() => import('./components/DataTable')),
editor: lazy(() => import('./components/Editor'))
};
// 4. 工具库层
const utils = {
date: () => import('date-fns'),
chart: () => import('chart.js'),
validation: () => import('yup')
};
// 应用结构
function App() {
return (
<BrowserRouter>
<Suspense fallback={<AppLoader />}>
<Routes>
<Route path="/" element={<routes.public />} />
<Route path="/user/*" element={<routes.user />} />
<Route path="/admin/*" element={<routes.admin />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}4.2 条件加载实战
javascript
// 基于权限的分割
function ProtectedRoute({ permission }) {
const { hasPermission } = useAuth();
if (!hasPermission(permission)) {
return <Redirect to="/login" />;
}
const Component = lazy(() =>
import(`./pages/${permission}Page`)
);
return (
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
);
}
// 基于功能开关的分割
function FeatureRoute({ feature }) {
const { isEnabled } = useFeatureFlags();
if (!isEnabled(feature)) {
return <NotFound />;
}
const Component = lazy(() =>
import(`./features/${feature}/Component`)
);
return (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
);
}
// 基于A/B测试的分割
function ABTestComponent({ experiment }) {
const { variant } = useABTest(experiment);
const Component = lazy(() =>
import(`./variants/${variant}Component`)
);
return (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
);
}4.3 渐进式迁移
javascript
// 旧代码逐步迁移到lazy
// Step 1: 识别大型组件
const OldHeavyComponent = require('./OldHeavyComponent');
// Step 2: 创建lazy版本
const NewLazyComponent = lazy(() => import('./OldHeavyComponent'));
// Step 3: 逐步替换
function App() {
const [useLazy, setUseLazy] = useState(false);
return (
<div>
{useLazy ? (
<Suspense fallback={<Loading />}>
<NewLazyComponent />
</Suspense>
) : (
<OldHeavyComponent />
)}
</div>
);
}
// Step 4: 特性开关控制
function GradualMigration() {
const { enableLazyLoading } = useFeatureFlags();
const Component = enableLazyLoading
? lazy(() => import('./NewComponent'))
: OldComponent;
return enableLazyLoading ? (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
) : (
<Component />
);
}注意事项
1. 避免过度分割
javascript
// ❌ 过度分割
const Button = lazy(() => import('./Button'));
const Input = lazy(() => import('./Input'));
const Text = lazy(() => import('./Text'));
// ✅ 合理分割
const FormComponents = lazy(() => import('./FormComponents'));2. Suspense边界
javascript
// ❌ 缺少Suspense
const LazyComp = lazy(() => import('./Comp'));
<LazyComp /> // 错误!
// ✅ 正确使用
<Suspense fallback={<Loading />}>
<LazyComp />
</Suspense>3. 错误处理
javascript
// ✅ 完善的错误处理
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>常见问题
Q1: lazy和动态import的区别?
A: lazy是React组件专用,动态import是通用的模块导入。
Q2: 代码分割影响SEO吗?
A: SSR时需特殊处理,使用loadable-components。
Q3: 如何调试分割后的代码?
A: 使用source map和webpack bundle analyzer。
Q4: chunk加载失败怎么办?
A: 实现错误边界和重试机制。
Q5: 如何优化chunk大小?
A: 配置webpack splitChunks,分析bundle。
Q6: 预加载和懒加载冲突吗?
A: 不冲突,可以智能预加载即将需要的代码。
Q7: 所有组件都应该lazy吗?
A: 不应该,只对大型或非首屏组件使用。
Q8: 如何测试代码分割?
A: 使用Lighthouse和Network面板分析。
Q9: Suspense可以嵌套吗?
A: 可以,实现渐进式加载。
Q10: React 19对代码分割有改进吗?
A: 更好的Suspense支持和服务器组件。
总结
核心要点
1. 代码分割优势
✅ 减少初始bundle
✅ 按需加载
✅ 提升性能
✅ 优化用户体验
2. 分割策略
✅ 路由级分割
✅ 组件级分割
✅ 库级分割
✅ 条件分割
3. 实现方案
✅ React.lazy
✅ 动态import
✅ Webpack配置
✅ SSR支持最佳实践
1. 分割原则
✅ 优先路由分割
✅ 大型组件分割
✅ 第三方库按需
✅ 避免过度分割
2. 性能优化
✅ 智能预加载
✅ 合理chunk大小
✅ 缓存策略
✅ 错误处理
3. 用户体验
✅ 优质fallback
✅ 渐进式加载
✅ 错误恢复
✅ 加载指示代码分割是现代React应用必备的性能优化手段,合理使用能显著提升应用加载速度和用户体验。