Suspense 和异步渲染
更新: 10/16/2025 字数: 0 字 时长: 0 分钟
什么是 Suspense
Suspense 是 React 提供的一个组件,用于优雅地处理异步操作,让你可以"等待"某些代码加载完成,并在等待时显示一个 loading 状态。
jsx
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>Suspense 的使用场景
1. 代码分割(Code Splitting)
jsx
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}2. 数据获取(Data Fetching)
jsx
// 使用支持 Suspense 的数据获取库(如 Relay、SWR)
function ProfilePage({ userId }) {
return (
<Suspense fallback={<Spinner />}>
<ProfileDetails userId={userId} />
<ProfileTimeline userId={userId} />
</Suspense>
);
}
function ProfileDetails({ userId }) {
// 读取数据时,如果数据未准备好,会"抛出" Promise
const user = resource.user.read(userId);
return <div>{user.name}</div>;
}3. 资源加载(Images、Fonts等)
jsx
// React 未来可能支持
<Suspense fallback={<ImagePlaceholder />}>
<Image src="large-image.jpg" />
</Suspense>Suspense 的工作原理
基本机制
Suspense 通过捕获 Promise 来工作:
javascript
// 简化的 Suspense 实现
class Suspense extends React.Component {
state = { isLoading: false };
componentDidCatch(error) {
// 如果 "error" 是一个 Promise
if (error instanceof Promise) {
this.setState({ isLoading: true });
error.then(() => {
// Promise resolve 后,重新渲染
this.setState({ isLoading: false });
});
} else {
// 真正的错误,向上传播
throw error;
}
}
render() {
if (this.state.isLoading) {
return this.props.fallback;
}
return this.props.children;
}
}Suspense-enabled 资源
要支持 Suspense,资源需要抛出 Promise:
javascript
// 创建支持 Suspense 的资源
function createResource(promise) {
let status = 'pending';
let result;
const suspender = promise.then(
data => {
status = 'success';
result = data;
},
error => {
status = 'error';
result = error;
}
);
return {
read() {
if (status === 'pending') {
// 抛出 Promise,触发 Suspense
throw suspender;
} else if (status === 'error') {
// 抛出错误,触发 Error Boundary
throw result;
} else if (status === 'success') {
// 返回数据
return result;
}
}
};
}
// 使用
const userResource = createResource(fetchUser(userId));
function UserProfile() {
// 如果数据未准备好,会抛出 Promise
const user = userResource.read();
return <div>{user.name}</div>;
}React.lazy 的实现
基本用法
jsx
import { lazy, Suspense } from 'react';
// lazy 接受一个返回 Promise 的函数
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}lazy 的实现原理
javascript
// 简化的 lazy 实现
function lazy(ctor) {
let Component = null;
let status = 'pending';
let result;
const payload = {
_status: status,
_result: result,
};
const lazyType = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: (payload) => {
if (payload._status === 'pending') {
const ctor = payload._result;
const thenable = ctor();
// 设置 Promise 的回调
thenable.then(
moduleObject => {
payload._status = 'resolved';
payload._result = moduleObject.default;
},
error => {
payload._status = 'rejected';
payload._result = error;
}
);
payload._result = thenable;
payload._status = 'pending';
}
if (payload._status === 'resolved') {
return payload._result;
} else {
// 抛出 Promise 或错误
throw payload._result;
}
},
};
return lazyType;
}
// React 渲染 lazy 组件时
function mountLazyComponent(current, workInProgress, elementType) {
const lazyComponent = elementType;
const payload = lazyComponent._payload;
const init = lazyComponent._init;
// 调用 init,可能抛出 Promise
let Component = init(payload);
// 渲染真正的组件
workInProgress.type = Component;
return reconcileChildren(current, workInProgress, Component);
}嵌套 Suspense
瀑布式加载问题
jsx
// ❌ 问题:瀑布式加载
function App() {
return (
<Suspense fallback={<Loading />}>
<ProfilePage />
</Suspense>
);
}
function ProfilePage() {
return (
<>
<Suspense fallback={<Spinner />}>
<ProfileDetails /> {/* 等待数据 1 */}
</Suspense>
<Suspense fallback={<Spinner />}>
<ProfileTimeline /> {/* 等待数据 2,但要等数据 1 加载完 */}
</Suspense>
</>
);
}
// 时间线:
// 0s: 开始加载 ProfileDetails
// 2s: ProfileDetails 完成,开始加载 ProfileTimeline
// 4s: ProfileTimeline 完成
// 总计:4秒并行加载
jsx
// ✅ 解决方案:提前开始加载
function App() {
return (
<Suspense fallback={<Loading />}>
<ProfilePage />
</Suspense>
);
}
function ProfilePage() {
// 提前创建资源,并行加载
const detailsResource = useMemo(() => fetchProfileDetails(), []);
const timelineResource = useMemo(() => fetchProfileTimeline(), []);
return (
<>
<Suspense fallback={<Spinner />}>
<ProfileDetails resource={detailsResource} />
</Suspense>
<Suspense fallback={<Spinner />}>
<ProfileTimeline resource={timelineResource} />
</Suspense>
</>
);
}
// 时间线:
// 0s: 同时开始加载 ProfileDetails 和 ProfileTimeline
// 2s: 两者都完成
// 总计:2秒SuspenseList(实验性)
jsx
import { SuspenseList } from 'react';
// 控制多个 Suspense 的显示顺序
function App() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Spinner />}>
<ProfileDetails />
</Suspense>
<Suspense fallback={<Spinner />}>
<ProfileTimeline />
</Suspense>
<Suspense fallback={<Spinner />}>
<ProfileTrivia />
</Suspense>
</SuspenseList>
);
}
// revealOrder 选项:
// - forwards:按顺序显示
// - backwards:反向显示
// - together:一起显示Suspense 与 Error Boundaries
配合使用
jsx
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary
fallback={<ErrorFallback />}
onError={(error, errorInfo) => {
logErrorToService(error, errorInfo);
}}
>
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<h2>出错了!</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}封装通用组件
jsx
function AsyncBoundary({ children, errorFallback, loadingFallback }) {
return (
<ErrorBoundary fallback={errorFallback}>
<Suspense fallback={loadingFallback}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// 使用
<AsyncBoundary
errorFallback={<ErrorMessage />}
loadingFallback={<Spinner />}
>
<UserProfile userId={userId} />
</AsyncBoundary>数据获取模式
传统模式:Fetch-on-Render
jsx
// ❌ 传统方式:渲染后获取(瀑布式)
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
if (!user) return <Spinner />;
return (
<>
<ProfileDetails user={user} />
<ProfileTimeline userId={user.id} />
</>
);
}
function ProfileTimeline({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetchPosts(userId).then(setPosts);
}, [userId]);
if (posts.length === 0) return <Spinner />;
return <div>{/* ... */}</div>;
}
// 时间线:
// 0s: 开始获取 user
// 2s: user 完成,开始获取 posts
// 4s: posts 完成Suspense 模式:Render-as-You-Fetch
jsx
// ✅ Suspense 方式:尽早获取(并行)
function fetchProfileData() {
return {
user: createResource(fetchUser()),
posts: createResource(fetchPosts()),
};
}
// 组件外部立即开始获取
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<Spinner />}>
<ProfileDetails resource={resource} />
<ProfileTimeline resource={resource} />
</Suspense>
);
}
function ProfileDetails({ resource }) {
const user = resource.user.read();
return <div>{user.name}</div>;
}
function ProfileTimeline({ resource }) {
const posts = resource.posts.read();
return <div>{/* ... */}</div>;
}
// 时间线:
// 0s: 同时开始获取 user 和 posts
// 2s: 两者都完成实际应用:带缓存的数据获取
jsx
// 简单的缓存实现
const cache = new Map();
function createCachedResource(key, fetcher) {
if (!cache.has(key)) {
cache.set(key, createResource(fetcher()));
}
return cache.get(key);
}
// 自定义 Hook
function useSuspenseQuery(key, fetcher) {
const resource = useMemo(
() => createCachedResource(key, fetcher),
[key]
);
return resource.read();
}
// 使用
function UserProfile({ userId }) {
const user = useSuspenseQuery(
`user-${userId}`,
() => fetchUser(userId)
);
return <div>{user.name}</div>;
}
function App({ userId }) {
return (
<Suspense fallback={<Loading />}>
<UserProfile userId={userId} />
</Suspense>
);
}Streaming SSR
传统 SSR 的问题
javascript
// 传统 SSR
// 服务器端:
const html = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="bundle.js"></script>
</body>
</html>
`);
// 问题:
// 1. 必须等待所有数据加载完成
// 2. 必须等待整个 HTML 生成完成
// 3. 客户端必须等待所有 JS 加载完成
// 4. 必须等待整个应用 hydrate 完成Streaming SSR with Suspense
javascript
import { renderToPipeableStream } from 'react-dom/server';
// 服务器端
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/main.js'],
onShellReady() {
// 外壳准备好了,开始流式传输
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onAllReady() {
// 所有内容准备好了
},
onError(error) {
console.error(error);
}
}
);
});
// App 组件
function App() {
return (
<html>
<head>
<title>My App</title>
</head>
<body>
<Header />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
<Footer />
</body>
</html>
);
}
// 流式传输过程:
// 1. 立即发送 Header
// 2. 发送 Spinner(Comments 的 fallback)
// 3. 发送 Footer
// 4. 当 Comments 准备好时,发送实际内容Selective Hydration
jsx
// 客户端
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document, <App />);
// React 会:
// 1. 优先 hydrate 用户正在交互的部分
// 2. 其他部分延迟 hydrate
// 3. 提高首次交互响应速度实际应用示例
路由级 Suspense
jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}条件式 Suspense
jsx
function ProductPage({ productId }) {
const [showReviews, setShowReviews] = useState(false);
return (
<div>
<ProductDetails productId={productId} />
<button onClick={() => setShowReviews(true)}>
显示评论
</button>
{showReviews && (
<Suspense fallback={<ReviewsLoader />}>
<Reviews productId={productId} />
</Suspense>
)}
</div>
);
}图片懒加载
jsx
function LazyImage({ src, alt }) {
const [imageSrc, setImageSrc] = useState(null);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => setImageSrc(src);
}, [src]);
if (!imageSrc) {
throw new Promise((resolve) => {
const img = new Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
resolve();
};
});
}
return <img src={imageSrc} alt={alt} />;
}
// 使用
<Suspense fallback={<ImagePlaceholder />}>
<LazyImage src="large-image.jpg" alt="Large" />
</Suspense>最佳实践
1. 合理设置 fallback
jsx
// ❌ 避免:fallback 太简单
<Suspense fallback={<div>...</div>}>
<Component />
</Suspense>
// ✅ 推荐:提供有意义的 loading 状态
<Suspense fallback={<ComponentSkeleton />}>
<Component />
</Suspense>
// 或使用 Skeleton Screen
function ComponentSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
);
}2. 避免不必要的 Suspense
jsx
// ❌ 避免:每个小组件都包裹 Suspense
function App() {
return (
<>
<Suspense fallback={<Spinner />}>
<Header />
</Suspense>
<Suspense fallback={<Spinner />}>
<Content />
</Suspense>
<Suspense fallback={<Spinner />}>
<Footer />
</Suspense>
</>
);
}
// ✅ 推荐:在合适的粒度使用
function App() {
return (
<>
<Header />
<Suspense fallback={<ContentLoader />}>
<Content />
</Suspense>
<Footer />
</>
);
}3. 预加载
jsx
// 预加载路由
const Dashboard = lazy(() => import('./Dashboard'));
function Navigation() {
const prefetchDashboard = () => {
// 鼠标悬停时预加载
import('./Dashboard');
};
return (
<nav>
<Link to="/dashboard" onMouseEnter={prefetchDashboard}>
Dashboard
</Link>
</nav>
);
}4. 处理错误
jsx
function App() {
return (
<ErrorBoundary
fallback={({ error, resetErrorBoundary }) => (
<div>
<h2>加载失败</h2>
<details>{error.message}</details>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)}
>
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}第三方库支持
SWR
jsx
import useSWR from 'swr';
function Profile({ userId }) {
const { data, error } = useSWR(
`/api/user/${userId}`,
fetcher,
{ suspense: true } // 启用 Suspense
);
// 不需要检查 loading 状态
// 由 Suspense 处理
return <div>{data.name}</div>;
}
// 使用
<Suspense fallback={<Loading />}>
<Profile userId={userId} />
</Suspense>React Query
jsx
import { useQuery } from '@tanstack/react-query';
function Posts() {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
suspense: true,
});
return <div>{data.map(/* ... */)}</div>;
}
// 使用
<Suspense fallback={<Loading />}>
<Posts />
</Suspense>总结
Suspense 的核心概念:
异步组件
- 通过抛出 Promise 来"暂停"渲染
- 显示 fallback 直到 Promise resolve
- 支持代码分割和数据获取
工作原理
- React.lazy 创建懒加载组件
- Suspense 捕获 Promise
- Promise resolve 后重新渲染
最佳实践
- 合理设置 fallback
- 避免瀑布式加载
- 配合 Error Boundary 使用
- 提前开始数据获取
高级特性
- Streaming SSR
- Selective Hydration
- 并发渲染
理解 Suspense 有助于:
- 优化应用加载性能
- 改善用户体验
- 简化异步状态管理
- 实现更好的 SSR