React中Suspense和lazy实现懒加载的几种场景
作者:JJCTO袁龙
本文用5大高频场景和万能决策模板,教你用React.lazy和Suspense实现路由、弹窗、图表等组件的按需加载,配合错误处理和预加载策略,让bundle体积大幅减小,加载速度翻倍
一句话总结:React.lazy 动态 import 组件实现代码分割,Suspense 包裹懒加载组件提供 loading fallback——两者配合让首屏只加载必要代码,按需加载路由级和组件级模块,bundle 体积和加载速度双赢!
一、Suspense 和 lazy 是什么?
问题背景:默认情况下 React 会把整个应用打包成一个 bundle,首屏加载所有代码,页面打开慢。React.lazy + Suspense 让你按需加载组件,首屏只加载必要代码。
| 特性 | 直接 import | React.lazy |
|---|---|---|
| 加载时机 | 首次打包即加载 | 渲染时才动态 import |
| bundle 体积 | 全部在一个文件 | 自动代码分割 |
| 需要 Suspense | 不需要 | 必须(提供 loading) |
| SSR 支持 | 完整 | 需要额外处理 |
| 适用场景 | 首屏组件 | 路由/弹窗/重图表 |
// ❌ 直接 import:所有代码打进一个 bundle import Dashboard from './pages/Dashboard' import Settings from './pages/Settings' import Reports from './pages/Reports' // 三个页面全在首屏 bundle 里,打开慢
二、5 大高频场景 & 代码示例
场景 1:路由级懒加载 → 最常见
// ✅ 正确:路由级 lazy + Suspense
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './Layout'
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const Profile = lazy(() => import('./pages/Profile'))
function App() {
return (
<BrowserRouter>
<Layout>
<Suspense fallback={<PageLoading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</Layout>
</BrowserRouter>
)
}
function PageLoading() {
return <div className="loading">页面加载中...</div>
}
// ❌ 错误:Suspense 包了整个 Layout,布局也跟着闪
function App() {
return (
<Suspense fallback={<div>加载中</div>}>
<BrowserRouter>
<Layout> {/* Layout 也会被 fallback 覆盖 */}
<Routes>...</Routes>
</Layout>
</BrowserRouter>
</Suspense>
)
}
场景 2:弹窗/抽屉懒加载 → 用了才加载
// ✅ 正确:弹窗组件 lazy,点击时才加载代码
import { lazy, Suspense, useState } from 'react'
const HeavyModal = lazy(() => import('./components/HeavyModal'))
function App() {
const [modalOpen, setModalOpen] = useState(false)
return (
<div>
<button onClick={() => setModalOpen(true)}>打开弹窗</button>
{modalOpen && (
<Suspense fallback={<div>弹窗加载中...</div>}>
<HeavyModal onClose={() => setModalOpen(false)} />
</Suspense>
)}
</div>
)
}
// ❌ 错误:首屏就 import 重的弹窗组件 import HeavyModal from './components/HeavyModal' // 首屏就加载
场景 3:错误处理 → 配合 ErrorBoundary
// ✅ 正确:lazy 组件加载失败时用 ErrorBoundary 兜底
import { lazy, Suspense, Component } from 'react'
const RemoteConfig = lazy(() => import('./components/RemoteConfig'))
class ErrorBoundary extends Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error) {
console.error('组件加载失败:', error)
}
render() {
if (this.state.hasError) {
return <div>组件加载失败,请刷新重试</div>
}
return this.props.children
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>加载中...</div>}>
<RemoteConfig />
</Suspense>
</ErrorBoundary>
)
}
// ❌ 错误:没有 ErrorBoundary,网络断了组件加载失败直接白屏
<Suspense fallback={<div>加载中...</div>}>
<RemoteConfig /> {/* 加载失败 → 白屏,没有兜底 */}
</Suspense>
场景 4:预加载策略 → 鼠标 hover 时加载
// ✅ 正确:hover 时预加载,点击时已就绪
import { lazy, Suspense, useState } from 'react'
const ChartModal = lazy(() => import('./components/ChartModal'))
function Dashboard() {
const [showChart, setShowChart] = useState(false)
// hover 时触发预加载
const handleMouseEnter = () => {
import('./components/ChartModal')
}
return (
<div>
<button
onClick={() => setShowChart(true)}
onMouseEnter={handleMouseEnter} // 预加载
>
查看图表
</button>
{showChart && (
<Suspense fallback={<div>图表加载中...</div>}>
<ChartModal onClose={() => setShowChart(false)} />
</Suspense>
)}
</div>
)
}
圦景 5:命名导出懒加载 → 包装函数处理
// ✅ 正确:组件是命名导出时,需要手动包装
import { lazy, Suspense } from 'react'
// ❌ 错误:命名导出不能直接用
// const Icon = lazy(() => import('./Icon')) // 默认导出才有效
// ✅ 正确:手动从模块中取命名导出
const Icon = lazy(() =>
import('./Icon').then(module => ({ default: module.Icon }))
)
const Header = lazy(() =>
import('./Header').then(module => ({ default: module.Header }))
)
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Header />
<Icon />
</Suspense>
)
}
// ❌ 错误:命名导出直接传给 lazy
const Icon = lazy(() => import('./Icon'))
// lazy 期望返回 { default: Component }
// 如果 Icon.tsx 里是 export const Icon = ...
// 则返回 { Icon: Component },没有 default → 报错
三、万能兜底:懒加载决策模板
| 场景 | 是否懒加载 | 原因 |
|---|---|---|
| 首屏组件(Layout/Header) | ❌ 直接 import | 首屏必加载,lazy 反而多一次请求 |
| 路由页面 | ✅ lazy | 用户可能不访问所有页面 |
| 弹窗/抽屉 | ✅ lazy | 用到才加载,省 bundle |
| 图表/编辑器 | ✅ lazy | 体积大,首屏不需要 |
| 小组件(Button/Icon) | ❌ 直接 import | 体积小,lazy 的 overhead 不值 |
| 第三方大库 | ✅ lazy | 几百 KB 的库按需加载 |
万能模板:
import { lazy, Suspense, Component } from 'react'
// 1. ErrorBoundary 兜底
class LazyError extends Component {
state = { hasError: false }
static getDerivedStateFromError() { return { hasError: true } }
render() {
return this.state.hasError
? <div>加载失败,<button onClick={() => location.reload()}>重试</button></div>
: this.props.children
}
}
// 2. lazy 组件
const HeavyPage = lazy(() => import('./pages/HeavyPage'))
// 3. Suspense + ErrorBoundary 双保险
function App() {
return (
<LazyError>
<Suspense fallback={<Skeleton />}>
<HeavyPage />
</Suspense>
</LazyError>
)
}
四、预防 checklist(不再踩坑)
- 首屏组件不要 lazy,直接 import
- lazy 组件必须被 Suspense 包裹,否则报错
- 网络可能失败的场景加 ErrorBoundary
- 命名导出的组件用
.then(m => ({ default: m.X }))包装 - Suspense 的 fallback 不要太简陋,用 Skeleton 提升体验
- 大型应用配合 webpack magic comment 预加载:
/* webpackPrefetch: true */ - 路由级 Suspense 放在
<Routes>外层,不要包 Layout - 不要在循环或条件中调用
lazy(),放在模块顶层
五、一句话总结
lazy 动态 import 做代码分割,Suspense 提供加载兜底,ErrorBoundary 提供错误兜底——三件套配合让大应用首屏只加载必要代码,按需加载不怕慢。
到此这篇关于React中Suspense和lazy实现懒加载的几种场景的文章就介绍到这了,更多相关React中Suspense和lazy懒加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
