05. 代码分割 - lazy 与 Suspense
一、5W1H 概述
| 维度 | 内容 |
|---|
| What | 动态导入组件,将代码拆分成独立的 chunk |
| Why | 减少首屏加载时间,按需加载 |
| When | 大型应用、路由页面、不常用的组件 |
| Where | 路由配置、组件导入处 |
| Who | 需要优化加载性能的开发者 |
| How | const LazyComponent = lazy(() => import('./Component'))+<Suspense> |
二、What - 什么是代码分割?
代码分割(Code Splitting)是将组件代码拆分成独立的 chunk,只在需要时加载。
核心 API
| API | 作用 |
|---|
React.lazy() | 动态导入组件 |
<Suspense> | 加载 fallback UI |
import { lazy, Suspense } from 'react'; const LazyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( <Suspense fallback={<div>加载中...</div>}> <LazyComponent /> </Suspense> ); }
三、Why - 为什么需要代码分割?
3.1 问题:首屏加载慢
所有组件打包成一个文件,首屏需要下载全部代码。
3.2 解决方案:按需加载
传统打包: ┌─────────────────────────────────────┐ │ bundle.js (5MB) │ └─────────────────────────────────────┘ 代码分割: ┌─────────┐ ┌─────────┐ ┌─────────┐ │ main.js │ │ about.js│ │contact.js│ │ (500KB) │ │ (200KB) │ │ (150KB) │ └─────────┘ └─────────┘ └─────────┘
四、When - 何时使用?
| 场景 | 推荐程度 | 说明 |
|---|
| 路由页面 | ✅ 强烈推荐 | 最常见场景 |
| 大型组件 | ✅ 推荐 | 减少首屏体积 |
| 模态框内容 | ✅ 推荐 | 用户点击时才加载 |
| 首页核心组件 | ❌ 不推荐 | 会增加加载延迟 |
| 小组件 | ❌ 不推荐 | 分割成本大于收益 |
五、Where - 在哪里使用?
src/ ├── pages/ │ ├── Home.jsx # 可能不需要懒加载 │ ├── About.jsx # 懒加载 │ ├── Dashboard.jsx # 懒加载 │ └── Settings.jsx # 懒加载 ├── components/ │ └── HeavyChart.jsx # 懒加载 └── App.jsx
六、Who - 谁需要使用?
需要优化加载性能的开发者。
七、How - 如何使用?
7.1 路由懒加载基础
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 Contact = lazy(() => import('./pages/Contact')); const Dashboard = lazy(() => import('./pages/Dashboard')); function App() { return ( <BrowserRouter> <Suspense fallback={<div className="loading">加载中...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </Suspense> </BrowserRouter> ); }
7.2 自定义加载组件
// components/PageLoader.jsx function PageLoader() { return ( <div className="page-loader"> <div className="spinner"></div> <p>加载页面中...</p> </div> ); } // components/LoadingSkeleton.jsx function LoadingSkeleton() { return ( <div className="skeleton"> <div className="skeleton-header"></div> <div className="skeleton-content"></div> </div> ); } // 使用 <Suspense fallback={<PageLoader />}> <Routes> {/* 路由配置 */} </Routes> </Suspense>
7.3 嵌套路由懒加载
const DashboardLayout = lazy(() => import('./layouts/DashboardLayout')); const Overview = lazy(() => import('./pages/Overview')); const Users = lazy(() => import('./pages/Users')); const Settings = lazy(() => import('./pages/Settings')); function App() { return ( <Suspense fallback={<PageLoader />}> <Routes> <Route path="/dashboard" element={<DashboardLayout />}> <Route index element={<Overview />} /> <Route path="users" element={<Users />} /> <Route path="settings" element={<Settings />} /> </Route> </Routes> </Suspense> ); }
7.4 命名导出组件懒加载
// components/Button.jsx export const Button = () => <button>按钮</button>; export const IconButton = () => <button>图标按钮</button>; // 懒加载命名导出 const Button = lazy(() => import('./components/Button').then(module => ({ default: module.Button })) ); const IconButton = lazy(() => import('./components/Button').then(module => ({ default: module.IconButton })) );
7.5 条件加载
const HeavyChart = lazy(() => import('./components/HeavyChart')); const PDFViewer = lazy(() => import('./components/PDFViewer')); function Dashboard({ showChart, showPDF }) { return ( <div> {showChart && ( <Suspense fallback={<div>加载图表...</div>}> <HeavyChart /> </Suspense> )} {showPDF && ( <Suspense fallback={<div>加载 PDF 查看器...</div>}> <PDFViewer /> </Suspense> )} </div> ); }
7.6 模态框懒加载
const EditModal = lazy(() => import('./modals/EditModal')); const DeleteConfirmModal = lazy(() => import('./modals/DeleteConfirmModal')); function DataTable() { const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); return ( <div> <button onClick={() => setShowEditModal(true)}>编辑</button> {showEditModal && ( <Suspense fallback={<div>加载弹窗...</div>}> <EditModal onClose={() => setShowEditModal(false)} /> </Suspense> )} {showDeleteModal && ( <Suspense fallback={<div>加载弹窗...</div>}> <DeleteConfirmModal onClose={() => setShowDeleteModal(false)} /> </Suspense> )} </div> ); }
7.7 错误处理
import { lazy, Suspense } from 'react'; // 带错误处理的懒加载 const Component = lazy(() => import('./HeavyComponent').catch(error => ({ default: () => <div>加载失败,请刷新页面</div> })) ); // ErrorBoundary 组件 class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return <div>组件加载失败,请刷新页面</div>; } return this.props.children; } } // 组合使用 <ErrorBoundary> <Suspense fallback={<PageLoader />}> <LazyComponent /> </Suspense> </ErrorBoundary>
7.8 预加载
// 鼠标悬停时预加载 const LazyComponent = lazy(() => import('./HeavyComponent')); function Component() { const preload = () => { import('./HeavyComponent'); // 触发预加载 }; return ( <div onMouseEnter={preload}> <Suspense fallback={<div>加载中...</div>}> <LazyComponent /> </Suspense> </div> ); } // 可见性预加载 function PreloadOnVisible({ children, componentPath }) { const ref = useRef(null); useEffect(() => { const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { import(componentPath); observer.disconnect(); } }); if (ref.current) observer.observe(ref.current); return () => observer.disconnect(); }, [componentPath]); return <div ref={ref}>{children}</div>; }
7.9 Webpack 魔法注释
// 自定义 chunk 名称 const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home') ); const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About') ); // 预加载 const Dashboard = lazy(() => import(/* webpackPrefetch: true */ './pages/Dashboard') ); // 预获取 const Admin = lazy(() => import(/* webpackPreload: true */ './pages/Admin') );
7.10 完整示例:电商应用路由
// App.jsx import { lazy, Suspense } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; 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 Orders = lazy(() => import('./pages/Orders')); function App() { return ( <BrowserRouter> <Suspense fallback={<PageLoader />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/products" element={<Products />} /> <Route path="/product/:id" element={<ProductDetail />} /> <Route path="/cart" element={<Cart />} /> <Route path="/checkout" element={<Checkout />} /> {/* 需要登录的路由 */} <Route path="/profile" element={ <PrivateRoute> <Profile /> </PrivateRoute> } /> <Route path="/orders" element={ <PrivateRoute> <Orders /> </PrivateRoute> } /> </Routes> </Suspense> </BrowserRouter> ); }
八、性能优化建议
8.1 合理分割
// ✅ 按路由分割 const UserProfile = lazy(() => import('./pages/UserProfile')); const UserSettings = lazy(() => import('./pages/UserSettings')); // ❌ 过度分割(组件很小) const Button = lazy(() => import('./components/Button')); const Input = lazy(() => import('./components/Input'));
8.2 使用 Suspense 嵌套
<Suspense fallback={<LayoutSkeleton />}> <Routes> <Route path="/" element={<Layout />}> <Route index element={ <Suspense fallback={<PageSkeleton />}> <Home /> </Suspense> } /> </Route> </Routes> </Suspense>
九、常见陷阱
9.1 忘记 Suspense
// ❌ 没有 Suspense 包裹 <Routes> <Route path="/" element={<LazyHome />} /> </Routes> // ✅ 必须用 Suspense 包裹 <Suspense fallback={<div>加载中...</div>}> <Routes> <Route path="/" element={<LazyHome />} /> </Routes> </Suspense>
9.2 在 Suspense 外使用 lazy
// ❌ 在 Suspense 外使用 const LazyComponent = lazy(() => import('./Component')); <LazyComponent /> // 报错 // ✅ 必须在 Suspense 内使用 <Suspense fallback={<div>加载中...</div>}> <LazyComponent /> </Suspense>
十、练习题
- 实现路由懒加载,将首页、关于、联系页面进行代码分割
- 实现一个模态框的懒加载
- 添加加载状态和错误处理
十一、小结
| 要点 | 说明 |
|---|
| React.lazy | 动态导入组件 |
| Suspense | 加载 fallback UI |
| 路由分割 | 最常见的使用场景 |
| 预加载 | 提前加载即将使用的组件 |
| 错误处理 | 使用 ErrorBoundary |