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. 预加载资源

使用 webpackPrefetchlink 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>
  );
}

相关 API

这篇文章有帮助吗?
这篇文章有帮助吗?