04-性能优化与最佳实践——05. 代码分割 - lazy 与 Suspense 05. 代码分割 - lazy 与 Suspense一、5W1H 概述维度内容What动态导入组件将代码拆分成独立的 chunkWhy减少首屏加载时间按需加载When大型应用、路由页面、不常用的组件Where路由配置、组件导入处Who需要优化加载性能的开发者Howconst LazyComponent lazy(() import(./Component))Suspense二、What - 什么是代码分割代码分割Code Splitting是将组件代码拆分成独立的 chunk只在需要时加载。核心 APIAPI作用React.lazy()动态导入组件Suspense加载 fallback UIimport { 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 classNameloading加载中.../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 classNamepage-loader div classNamespinner/div p加载页面中.../p /div ); } // components/LoadingSkeleton.jsx function LoadingSkeleton() { return ( div classNameskeleton div classNameskeleton-header/div div classNameskeleton-content/div /div ); } // 使用 Suspense fallback{PageLoader /} Routes {/* 路由配置 */} /Routes /Suspense7.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 pathusers element{Users /} / Route pathsettings 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 /ErrorBoundary7.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 /Suspense9.2 在 Suspense 外使用 lazy// ❌ 在 Suspense 外使用 const LazyComponent lazy(() import(./Component)); LazyComponent / // 报错 // ✅ 必须在 Suspense 内使用 Suspense fallback{div加载中.../div} LazyComponent / /Suspense十、练习题实现路由懒加载将首页、关于、联系页面进行代码分割实现一个模态框的懒加载添加加载状态和错误处理十一、小结要点说明React.lazy动态导入组件Suspense加载 fallback UI路由分割最常见的使用场景预加载提前加载即将使用的组件错误处理使用 ErrorBoundary