说说对 Suspense 和懒加载的理解?
一、是什么
Suspense 是 React 提供的一种声明式加载状态管理机制,它允许组件在等待异步操作(如代码加载、数据获取)完成时,显示一个备用的 fallback UI。
React.lazy 是与 Suspense 配合使用的懒加载函数,它利用动态 import() 实现组件级别的代码分割(Code Splitting),将组件的代码从主包中拆分出去,按需加载。
两者结合,可以在不牺牲用户体验的前提下显著减小应用的初始加载体积。
二、React.lazy 基础
2.1 基本用法
import { lazy, Suspense } from 'react';
// lazy 接收一个返回 Promise<{ default: Component }> 的函数
const LazyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</Suspense>
);
}
React.lazy 的工作原理:
- 首次渲染时调用
() => import('./HeavyComponent')
- 动态
import() 返回一个 Promise
- 在 Promise resolve 之前,React 向上查找最近的
Suspense 边界,显示 fallback
- Promise resolve 后,React 用加载完成的组件替换 fallback
2.2 命名导出的处理
React.lazy 要求模块默认导出一个组件。对于命名导出,需要中间层转换:
// utils.js
export function formatDate() { /* ... */ }
export function DataTable() { return <table>...</table>; }
// 使用方式:通过中间 Promise 转换
const LazyDataTable = lazy(() =>
import('./utils').then(module => ({ default: module.DataTable }))
);
三、Suspense 边界
3.1 fallback UI
Suspense 的 fallback prop 接受任何 React 元素,从简单文字到复杂的骨架屏组件。
3.2 嵌套 Suspense
多个 Suspense 边界可以嵌套使用,实现更细粒度的加载状态控制:
const Header = lazy(() => import('./Header'));
const Sidebar = lazy(() => import('./Sidebar'));
const MainContent = lazy(() => import('./MainContent'));
const Comments = lazy(() => import('./Comments'));
function App() {
return (
<Suspense fallback={<FullPageSpinner />}>
<Header />
<div className="layout">
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<main>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
<Suspense fallback={<p>加载评论...</p>}>
<Comments />
</Suspense>
</Suspense>
</main>
</div>
</Suspense>
);
}
嵌套策略:
- 最外层 Suspense 捕获整体布局级别的挂起
- 内部 Suspense 为各区域提供独立的加载指示
- 如果内层 Suspense 已就绪但外层未就绪,仍显示外层 fallback
3.3 Suspense 与并发渲染
在使用 startTransition 时,Suspense 的行为有特殊变化——它不会立即回退到 fallback,而是保持显示旧内容:
import { useState, useTransition, Suspense, lazy } from 'react';
const PostsPage = lazy(() => import('./PostsPage'));
const PhotosPage = lazy(() => import('./PhotosPage'));
function Router() {
const [page, setPage] = useState('posts');
const [isPending, startTransition] = useTransition();
function navigate(to) {
startTransition(() => {
setPage(to);
});
}
return (
<>
<nav>
<button onClick={() => navigate('posts')}>文章</button>
<button onClick={() => navigate('photos')}>相册</button>
</nav>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Suspense fallback={<Loading />}>
{page === 'posts' ? <PostsPage /> : <PhotosPage />}
</Suspense>
</div>
</>
);
}
四、路由级代码分割
路由是最常见的代码分割点,每个路由对应一个懒加载组件:
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 Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<nav>{/* 导航链接 */}</nav>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner" />
<p>页面加载中...</p>
</div>
);
}
预加载优化
可以在用户可能导航之前提前触发加载:
const AboutPage = lazy(() => import('./pages/About'));
function NavLink() {
function preload() {
import('./pages/About');
}
return (
<a
href="/about"
onMouseEnter={preload}
onFocus={preload}
>
关于我们
</a>
);
}
五、Suspense 与数据获取
React 18 开始正式支持在 Suspense 中使用数据获取。搭配支持 Suspense 的框架(如 Next.js、Relay)以及 React 19 的 use() Hook,可以实现声明式的数据加载:
import { use, Suspense } from 'react';
function ProfilePage({ userId }) {
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
return (
<div>
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts postsPromise={postsPromise} />
</Suspense>
</div>
);
}
function ProfileDetails({ userPromise }) {
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
六、Error Boundary + Suspense
加载过程中可能出错,需要 Error Boundary 配合处理:
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
);
}
推荐的嵌套顺序:ErrorBoundary 在外,Suspense 在内。这样加载失败时 Error Boundary 能捕获错误并提供重试机制。
七、SSR 流式渲染与 Suspense
React 18 在服务端渲染中对 Suspense 进行了重大增强——支持流式 HTML 和选择性水合:
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
});
});
流式渲染的工作流程:
- 服务端先发送 Shell HTML(已就绪部分 + Suspense fallback)
- 各 Suspense 边界内的数据就绪后,通过流发送对应 HTML 替换 fallback
- 客户端选择性水合——用户交互的区域优先
八、总结
Suspense 和 React.lazy 代表了 React 对异步操作的声明式抽象——开发者只需声明"等待什么"和"等待时显示什么",React 负责处理加载、渲染和状态切换的全部细节。