单页应用(SPA)随着功能增多,打包后的 JavaScript 文件会越来越庞大。如果用户首次加载时就下载整个应用的所有代码,会导致白屏时间长、首屏体验差。路由懒加载与代码分割正是解决这一问题的核心手段:仅加载当前页面所需的代码,其他页面的代码按需加载。
为什么需要路由懒加载
假设你的应用包含首页、用户管理、报表分析、系统设置四个模块,每个模块有自己独立的组件树和依赖。如果不做代码分割:
- 打包后的 bundle.js 可能超过 1MB。
- 用户访问首页时,却要强制下载报表分析、系统设置等暂时用不到的代码。
- 在弱网环境下,加载时间可能长达数秒,严重影响用户体验。
路由懒加载让每个页面成为独立的代码块(chunk),当用户导航到某个页面时,才动态加载该页面对应的 JavaScript 文件。这样做带来的直接好处:
- 首屏加载更快:初始只加载必要代码。
- 按需加载:用户访问得越深,才加载相应模块,节省带宽。
- 缓存更有效:某个页面的代码更新,只影响对应 chunk,其他 chunk 仍可被浏览器缓存。
实现方式:React.lazy + Suspense + 动态 import
React 提供了 React.lazy() 函数用于定义懒加载组件,配合 Suspense 处理加载中的状态。结合 React Router v6,可以轻松实现路由级别的代码分割。
基础示例
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 使用动态 import() 懒加载页面组件
const HomePage = lazy(() => import('./pages/HomePage'));
const UserManagePage = lazy(() => import('./pages/UserManagePage'));
const ReportPage = lazy(() => import('./pages/ReportPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>页面加载中...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/users" element={<UserManagePage />} />
<Route path="/reports" element={<ReportPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
当用户访问 /users 时,浏览器才会请求 UserManagePage 对应的 JS 文件。在加载期间,Suspense 的 fallback 属性指定的内容(如加载提示、骨架屏)会显示出来,直到组件加载完成后自动替换为实际内容。
在实际项目中的最佳实践
1. 统一管理路由配置
通常会将路由配置抽离成一个数组,方便维护和批量生成:
import { lazy } from 'react';
const routes = [
{
path: '/',
element: lazy(() => import('./pages/HomePage')),
},
{
path: '/users',
element: lazy(() => import('./pages/UserManagePage')),
children: [
{
path: ':id',
element: lazy(() => import('./pages/UserDetailPage')),
},
],
},
// ...其他路由
];
在渲染时遍历 routes 生成 <Route> 元素。这种方式让路由管理集中在单一文件,增删路由一目了然。
2. 使用更友好的 fallback 界面
简单的 "加载中..." 文字可能让用户感觉生硬,建议使用骨架屏或品牌化的加载动画:
function LoadingSpinner() {
return (
<div className="loading-container">
<Spin size="large" tip="正在努力加载页面..." />
</div>
);
}
// 在 Suspense 中使用
<Suspense fallback={<LoadingSpinner />}>
<Routes>{/* ... */}</Routes>
</Suspense>
对于企业级应用,保持加载态的品牌统一性可以提升整体体验。
3. 错误边界兜底
懒加载也可能因为网络错误、chunk 文件丢失等原因加载失败。仅用 Suspense 无法捕获这类错误,需要配合错误边界(Error Boundary):
class ChunkErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<ErrorResult
title="页面加载失败"
description="请检查网络连接或刷新重试"
onRetry={this.handleRetry}
/>
);
}
return this.props.children;
}
}
// 包裹 Suspense
<ChunkErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Routes>{/* ... */}</Routes>
</Suspense>
</ChunkErrorBoundary>
这样即使加载失败,用户也能看到友好的提示并提供重试按钮,而不是面对一个白屏。
4. 嵌套路由的懒加载
React Router v6 支持嵌套路由,子路由也可以懒加载。注意,父组件加载后才能匹配子路由,因此父组件本身也需要被 lazy 包裹,或者使用 Suspense 包裹嵌套的 <Outlet />:
// UserLayout.tsx
import { Outlet } from 'react-router-dom';
export default function UserLayout() {
return (
<div>
<h2>用户管理</h2>
<Suspense fallback={<p>加载用户详情...</p>}>
<Outlet />
</Suspense>
</div>
);
}
这种方式能进一步精细控制加载粒度,避免加载一个父页面时连带下载所有子页面。
与构建工具的配合
代码分割依赖于构建工具(如 Vite、Webpack)对动态 import() 语法的支持。当你使用 lazy(() => import('./pages/UserManagePage')) 时,构建工具会自动将该文件及其依赖提取为独立的 chunk 文件。
Vite 的代码分割默认基于动态导入,无需额外配置。若想手动控制 chunk 名称,可以使用魔法注释:
const UserPage = lazy(() => import(/* webpackChunkName: "user-page" */ './pages/UserPage'));
Vite 则通过输出配置来控制:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
}
};
合理拆分 vendor(第三方库)和应用代码,可以进一步优化缓存和加载性能。
什么时候不需要懒加载
路由懒加载虽然好,但并非所有页面都值得拆分:
- 首屏关键页面:比如首页本身就是用户最先看到的,懒加载会增加一次额外的请求,反而可能拖慢首屏。这类页面可与主入口打包在一起,或使用预加载策略。
- 极小页面:几 KB 的组件拆分后的请求开销可能比代码本身还大,可以保留在同一个 bundle 中。
- 高频访问且体积不大的页面:频繁的下载请求反而增加网络负担,可以结合预加载(preload/prefetch)优化。
使用预加载提升体验
对于可能在后续流程中访问的页面,可以在用户悬停或空闲时提前加载其 chunk:
import { lazy } from 'react';
const SettingsPage = lazy(() => import('./pages/SettingsPage'));
// 在用户鼠标移入导航菜单时预加载
<Link
to="/settings"
onMouseEnter={() => {
import(/* webpackPrefetch: true */ './pages/SettingsPage');
}}
>
系统设置
</Link>
Webpack 的 / webpackPrefetch: true / 注释会让浏览器在空闲时预加载该资源,这样当用户真正点击时,组件几乎瞬间可用。在 Vite 中,你可以使用 <link rel="prefetch"> 或 import() 提前触发请求。
总结
路由懒加载与代码分割是现代前端应用性能优化的标配手段。核心要点:
- 使用
React.lazy和动态import()定义懒加载组件。 - 用
Suspense提供加载态 UI。 - 配合错误边界处理加载异常,保证应用健壮性。
- 结合构建工具合理拆分 chunk,平衡加载颗粒度。
- 对于关键页面考虑预加载,避免影响导航体验。
实施路由懒加载后,你的应用将具备“按需分配”的能力,让用户只为他们当前看到的内容买单,显著提升首屏加载速度和整体性能。