引言
在现代前端开发中,加载体验是衡量应用质量的关键指标。传统的加载方式往往导致白屏或 loading 闪烁,而 React 16.6 引入的 Suspense 组件为异步渲染提供了全新的解决方案。它允许组件在数据或代码就绪前“挂起”,优雅地展示 fallback UI。本文将从基础概念讲起,逐步深入,结合实践案例,教你如何用 Suspense 优化加载体验,并避开常见陷阱。
什么是 React Suspense?
Suspense 是 React 提供的一个组件,用于“等待”某些异步操作完成。它有两个核心应用场景:
- 代码分割:配合
React.lazy()实现组件的按需加载。 - 数据获取:配合数据请求库(如 TanStack Query、SWR)或自定义的 Suspense 数据源,在数据就绪前显示 fallback。
基础用法
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
当 LazyComponent 尚未加载完成时,React 会渲染 fallback 内容;加载完成后,则渲染真实组件。
实战:用 Suspense 优化代码分割
场景描述
假设我们有一个大型应用,包含多个路由页面。传统做法是打包成一个巨大的 bundle,导致首屏加载缓慢。使用 Suspense + React.lazy() 可以按需加载每个页面。
步骤 1:创建懒加载页面
// pages/Home.js
export default function Home() {
return <h1>Home Page</h1>;
}
// pages/About.js
export default function About() {
return <h1>About Page</h1>;
}
步骤 2:使用 React.lazy 动态导入
// App.js
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { Suspense, lazy } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Router>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<Suspense fallback={<div>页面加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
效果:首次访问 / 时只加载 Home 相关代码,访问 /about 时再加载 About。
步骤 3:优化 fallback 体验
避免 fallback 闪烁,可以使用 CSS 过渡或延迟显示。
// 自定义 fallback 组件
import { useState, useEffect } from 'react';
function DelayedFallback() {
const [show, setShow] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setShow(true), 200);
return () => clearTimeout(timer);
}, []);
return show ? <div>Loading...</div> : null;
}
然后 。这样如果加载很快(<200ms),就不会闪烁。
进阶:Suspense 用于数据获取
场景描述
在获取用户信息时,我们希望组件能像等待代码一样等待数据。这需要数据源支持 Suspense 模式。目前主流方案是使用 TanStack Query 或 SWR,它们内置了 Suspense 支持。
使用 TanStack Query 的 Suspense 模式
首先安装:
npm install @tanstack/react-query
配置 QueryClient 并启用 Suspense:
// App.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Suspense } from 'react';
import UserProfile from './UserProfile';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // 全局启用 Suspense 模式
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userId={1} />
</Suspense>
</QueryClientProvider>
);
}
export default App;
创建 UserProfile 组件:
// UserProfile.js
import { useQuery } from '@tanstack/react-query';
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default function UserProfile({ userId }) {
const { data } = useQuery(['user', userId], () => fetchUser(userId));
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
);
}
注意:在 Suspense 模式下,useQuery 不会返回 isLoading,因为它会直接挂起组件。
自定义 Suspense 数据源
如果你不想依赖第三方库,可以自己实现一个简单的 Suspense 数据源。核心思想是:如果数据未准备好,就抛出一个 Promise。
// createResource.js
function createResource(promise) {
let status = 'pending';
let result;
let error;
const suspender = promise.then(
(data) => {
status = 'success';
result = data;
},
(err) => {
status = 'error';
error = err;
}
);
return {
read() {
if (status === 'pending') {
throw suspender; // 抛出一个 Promise,让 Suspense 捕获
} else if (status === 'error') {
throw error;
} else if (status === 'success') {
return result;
}
},
};
}
export default createResource;
然后这样使用:
// UserProfile.js
import createResource from './createResource';
const userResource = createResource(fetchUser(1)); // 注意:这里在模块顶层调用,但实际中应在组件内或缓存中
export default function UserProfile() {
const user = userResource.read(); // 如果未完成,会抛出 promise,被 Suspense 捕获
return (
<div>
<h2>{user.name}</h2>
</div>
);
}
注意:在模块顶层创建资源会立即发起请求,而且不会重新获取。实际中应使用状态管理或缓存,并注意每次渲染的稳定性。
错误处理:ErrorBoundary 与 Suspense 结合
Suspense 只处理加载中状态,如果异步操作出错,需要配合 ErrorBoundary。
创建 ErrorBoundary
// ErrorBoundary.js
import { Component } from 'react';
export default class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Caught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong.</div>;
}
return this.props.children;
}
}
使用 ErrorBoundary 包裹 Suspense
import ErrorBoundary from './ErrorBoundary';
import { Suspense } from 'react';
import UserProfile from './UserProfile';
function App() {
return (
<ErrorBoundary fallback={<div>用户信息加载失败</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
这样,当数据请求失败时,错误边界会捕获并显示错误 UI。
最佳实践与常见坑
1. 不要在组件内部创建新的 Promise 或 Resource
如果直接在组件内调用 createResource(fetchUser(id)),每次渲染都会创建新的 Promise,导致无限循环。正确做法是使用 useState 或 useMemo 缓存资源,或使用像 TanStack Query 这样的库。
错误示例:
export default function UserProfile({ userId }) {
const resource = createResource(fetchUser(userId)); // 每次渲染都新建!
const user = resource.read();
return <div>{user.name}</div>;
}
正确示例(使用 useMemo):
export default function UserProfile({ userId }) {
const resource = useMemo(() => createResource(fetchUser(userId)), [userId]);
const user = resource.read();
return <div>{user.name}</div>;
}
2. 多个 Suspense 组件嵌套时的行为
当多个 Suspense 嵌套时,React 会从最近的 Suspense 边界开始处理。通常建议在路由级别使用一个大的 Suspense,而不是每个组件都包裹。
3. 与 React 18 的并发特性结合
React 18 增强了 Suspense 的能力,支持 useTransition 和 startTransition,可以避免已显示内容的突然消失。
import { useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('home');
function switchTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('about')}>About</button>
{isPending ? '...' : null}
<Suspense fallback={<div>Loading...</div>}>
{tab === 'home' ? <Home /> : <About />}
</Suspense>
</div>
);
}
4. 避免 fallback 闪烁
在快速加载场景下,fallback 会一闪而过,造成视觉抖动。解决方案是延迟显示 fallback(如前面提到的 DelayedFallback),或者使用 useDeferredValue 或 useTransition 来保持旧 UI。
总结与展望
React Suspense 是构建高性能加载体验的利器。通过本文的学习,你已经掌握:
- 使用
React.lazy和 Suspense 实现代码分割 - 使用 Suspense 模式的数据请求库(如 TanStack Query)
- 自定义 Suspense 数据源
- 结合 ErrorBoundary 处理错误
- 避免常见陷阱的实践经验
下一步:可以深入学习 React 18 的并发特性(如 useTransition、useDeferredValue),以及服务端渲染(SSR)中的 Suspense 支持。Suspense 的未来将更加智能,期待你用它打造出色的用户体验。
> 代码示例完整可用,请在实际项目中测试调整。