React lazy与Suspense配合路由实现按需加载页面
随着前端应用的功能日益丰富,打包后的 JavaScript 体积往往会变得非常庞大。如果用户打开网页时需要一次性下载所有代码,首屏加载时间就会变长,用户体验也会随之下降。为了解决这个问题,我们需要使用代码分割技术,将代码拆分成不同的“块”,按需加载。
React 提供了 React.lazy 和 Suspense 两个核心 API,配合路由系统(如 React Router),可以极其轻松地实现页面的按需加载。以下将手把手教你如何配置和优化这一过程。
核心流程概览
在开始写代码之前,我们需要理清“懒加载”在路由层面的工作逻辑。当用户点击导航链接跳转时,浏览器会检查目标页面所需的代码是否已经下载。
如果未下载,则进入加载状态,显示预设的 Loading 界面;一旦下载完成,React 会自动渲染真正的页面组件。
第一步:准备基础路由结构
首先,我们需要一个标准的路由配置作为对比起点。假设我们使用的是 React Router v6,且已经安装好了相关依赖。
创建 一个名为 App.jsx 的文件,并输入 以下常规的路由配置代码。这是一个“未优化”的版本,所有页面组件都会在应用启动时被同步加载。
// App.jsx (未优化版本)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Dashboard from './pages/Dashboard';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
在这个阶段,无论用户访问哪个页面,Home、About 和 Dashboard 三个页面的代码都会被打包进同一个 main.js 文件中。
第二步:创建 Loading 占位组件
在使用懒加载时,网络下载组件代码需要时间。在这段时间内,我们不能让页面白屏,因此需要一个 Loading 组件来告诉用户“正在加载中”。
新建 一个文件 Loading.jsx,并编写 如下简单的 UI 代码:
// Loading.jsx
export default function Loading() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '20px',
color: '#666'
}}>
页面加载中,请稍候...
</div>
);
}
你可以根据实际项目需求,将其替换为旋转的 Spinner 动画或骨架屏。
第三步:使用 React.lazy 转换导入方式
React.lazy 允许你动态导入组件。它接受一个函数作为参数,这个函数需要动态调用 import()。这会自动返回一个 Promise,该 Promise resolve 为一个包含默认导出 React 组件的模块。
修改 App.jsx 文件顶部的 import 语句。
删除 原有的静态导入语句:
// 删除这些
// import Home from './pages/Home';
// import About from './pages/About';
// import Dashboard from './pages/Dashboard';
引入 React 和 Suspense,并添加 动态导入代码:
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { Suspense } from 'react';
import Loading from './Loading';
// 使用 React.lazy 动态导入组件
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
注意:React.lazy 目前只支持默认导出。如果你的页面组件使用了命名导出,你需要创建一个中间模块,先进行默认导出,然后再 lazy 导入这个中间模块。
第四步:使用 Suspense 包裹路由
由于 React.lazy 加载的组件在渲染时可能会处于“正在加载”的状态,我们需要在组件外层包裹 Suspense 组件。Suspense 的作用是捕获其子组件树的加载状态,并在子组件未准备好时显示 fallback 属性指定的内容。
修改 App.jsx 中的 return 部分,将 <Routes> 用 <Suspense> 包裹起来:
// App.jsx
export default function App() {
return (
<BrowserRouter>
{/* Suspense 包裹所有懒加载的路由 */}
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
此时,当你访问 /about 路径时,React 会暂停渲染 About 组件,转而渲染 Loading 组件,直到 about.js 文件下载完毕。
第五步:验证代码分割效果
仅仅修改代码是不够的,我们需要通过浏览器的开发者工具来确认“按需加载”是否真的生效了。
执行 以下操作步骤进行验证:
- 启动 项目开发环境或生产构建。
- 打开 Chrome 浏览器,按下
F12键打开开发者工具。 - 点击 顶部的
Network标签页。 - 勾选
Network标签页下的Disable cache选项(防止缓存干扰测试)。 - 刷新 页面,确保加载的是主页。
此时观察 Network 面板中的 JS 文件列表。你应该会看到类似 main.chunk.js 这样的主文件。
- 点击 应用中的“关于”链接跳转到
/about。 - 观察
Network面板,你会发现浏览器发起了一个新的请求,下载了一个名为类似src_pages_about_js.chunk.js的文件。
这正是我们想要的效果:关于页面的代码被拆分到了独立的文件中,只有在你真正访问它时才会被下载。
第六步:进阶优化(处理命名导出与错误边界)
虽然 React.lazy 非常好用,但它有两个常见的限制需要处理。
1. 处理命名导出
如果你的页面组件是这样写的:
// pages/Profile.jsx
export const Profile = () => { ... };
直接使用 React.lazy(() => import('./pages/Profile')) 会报错,因为它期待的是 export default。
解决 方法是创建一个中间文件 ProfileWrapper.jsx:
// pages/ProfileWrapper.jsx
export { Profile as default } from './Profile';
然后在路由中 lazy 导入这个中间文件:
const Profile = React.lazy(() => import('./pages/ProfileWrapper'));
2. 处理加载失败
如果网络中断或者服务器崩溃,动态导入的 Promise 可能会 reject。为了避免应用白屏,建议使用错误边界来捕获这些错误。
创建 一个 ErrorBoundary.jsx 组件:
// ErrorBoundary.jsx
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1>页面加载失败,请刷新重试。</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
调整 App.jsx,将 ErrorBoundary 放置在 Suspense 的外层:
import ErrorBoundary from './ErrorBoundary';
// ... 其他 import
export default function App() {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<Routes>
{/* ... 路由配置 ... */}
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}
总结对比
通过上述步骤,我们已经完成了路由懒加载的全部配置。为了更直观地理解优化前后的区别,请参考下表。
| 特性 | 优化前 (同步导入) | 优化后 (React.lazy) |
|---|---|---|
| 打包结果 | 所有页面代码合并进一个巨大的 JS 文件 | 每个路由页面生成独立的 .chunk.js 文件 |
| 首屏加载 | 必须下载所有代码,首屏速度慢 | 仅下载首页代码,首屏速度快 |
| 用户体验 | 切换路由无等待,但初始加载慢 | 首次切到新路由时可能有短暂 Loading |
| 缓存策略 | 只要一个文件变动,整个文件缓存失效 | 单个页面变动,只重新下载该页面的 Chunk |
| 适用场景 | 极小型应用 | 中大型应用,特别是管理后台或多页面站点 |

暂无评论,快来抢沙发吧!