Suspense
Suspense 让你声明加载状态,在组件等待某些内容时显示后备 UI。
概述
Suspense 让你的组件在等待某些操作(如数据获取、代码分割、图片加载) 完成之前"等待"并显示加载指示器。
Suspense 可以用于:
- 代码分割 - 动态导入组件
- 数据获取 - 配合数据框架使用
- 懒加载图片和其他资源
💡 提示: Suspense 是 React 并发特性的基础, 与 Suspense 配合的代码分割和数据获取库会自动处理加载状态。
基本用法
Suspense 组件接受一个 fallback prop,在子组件加载完成前显示。
<Suspense fallback={<Loading />}>
<ComponentThatWaits />
</Suspense>代码分割
Suspense 最常见的用途是与 React.lazy() 配合进行代码分割。
import { Suspense } from 'react';
import React, { lazy } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}✅ 优点: 代码分割可以显著减少初始加载体积, 只在需要时才加载对应的代码块。
路由懒加载
在路由配置中使用 Suspense 实现页面级别的代码分割:
import { Suspense, lazy } 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
你可以嵌套多个 Suspense 组件,每个可以有自己的 fallback:
function ProfilePage() {
return (
<Suspense fallback={<BigSpinner />}>
<NavBar />
<Suspense fallback={<PostsSkeleton />}>
<PostsTab />
</Suspense>
<Suspense fallback={<GallerySkeleton />}>
<GalleryTab />
</Suspense>
</Suspense>
);
}这样可以让每个部分独立显示加载状态,提升用户体验。
数据获取(实验性)
Suspense 也可以用于数据获取,但需要支持 Suspense 的数据库或框架:
import { Suspense } from 'react';
// 数据获取函数
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// 组件使用数据
function UserProfile({ userId }) {
// 当数据还未加载时,这个组件会"suspend"
const user = fetchUserData(userId);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// 使用 Suspense 包裹
function App() {
return (
<Suspense fallback={<div>加载用户数据...</div>}>
<UserProfile userId={1} />
</Suspense>
);
}⚠️ 注意: 直接在组件中使用 fetch 仍需要其他库的支持。 推荐使用 Relay、Next.js 或其他集成了 Suspense 的框架。
加载指示器设计
好的加载指示器可以显著提升用户体验。以下是一些设计建议:
// 简单的加载指示器
function Spinner() {
return <div className="spinner" />;
}
// 带进度的加载指示器
function LoadingProgress() {
return (
<div className="loading-container">
<div className="spinner" />
<p>加载中...</p>
</div>
);
}
// 骨架屏
function PostSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-title" />
<div className="skeleton-text" />
<div className="skeleton-text" />
</div>
);
}
// 使用
<Suspense fallback={<PostSkeleton />}>
<Post />
</Suspense>错误处理
当 Suspense 内的组件抛出错误时,应该使用错误边界(Error Boundary)捕获:
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <div>出错了: {this.state.error.message}</div>;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>加载中...</div>}>
<ComponentThatMayFail />
</Suspense>
</ErrorBoundary>
);
}最佳实践
✅ 推荐做法
- 使用骨架屏(skeleton)而不是简单的加载文字
- 嵌套 Suspense 以优化不同部分的加载体验
- 配合 Error Boundary 处理错误情况
- 为路由级别的懒加载添加 Suspense
- 保持 fallback UI 简洁快速
❌ 避免做法
- 不要在 fallback 中进行复杂计算或数据获取
- 不要过度嵌套 Suspense(保持合理层级)
- 不要忘记处理错误情况
- 不要在 useEffect 中使用 Suspense(不推荐)
性能优化
使用 Suspense 时的一些性能优化技巧:
1. 预加载资源
使用 webpackPrefetch 或 link rel="preload" 预加载资源:
// 预加载组件 const HeavyComponent = lazy(() => import( /* webpackPrefetch: true */ './HeavyComponent' ));
2. 合理的代码分割
将代码分割成合理的块,避免太小的分割:
// ✅ 好: 按路由分割
const Dashboard = lazy(() => import('./routes/Dashboard'));
// ❌ 差: 过小的分割(每个组件)
const Button = lazy(() => import('./Button'));
const Input = lazy(() => import('./Input'));3. 避免布局抖动
使用骨架屏保持页面布局稳定:
// 骨架屏应该与真实内容尺寸相似
function PostSkeleton() {
return (
<div className="post" style={{ minHeight: '200px' }}>
<div className="skeleton-title" />
<div className="skeleton-content" />
</div>
);
}