04-性能优化与最佳实践——05. 代码分割 - lazy 与 Suspense

05. 代码分割 - lazy 与 Suspense

一、5W1H 概述

维度内容
What动态导入组件,将代码拆分成独立的 chunk
Why减少首屏加载时间,按需加载
When大型应用、路由页面、不常用的组件
Where路由配置、组件导入处
Who需要优化加载性能的开发者
Howconst 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>

十、练习题

  1. 实现路由懒加载,将首页、关于、联系页面进行代码分割
  2. 实现一个模态框的懒加载
  3. 添加加载状态和错误处理

十一、小结

要点说明
React.lazy动态导入组件
Suspense加载 fallback UI
路由分割最常见的使用场景
预加载提前加载即将使用的组件
错误处理使用 ErrorBoundary