说说对 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 的工作原理:

  1. 首次渲染时调用 () => import('./HeavyComponent')
  2. 动态 import() 返回一个 Promise
  3. 在 Promise resolve 之前,React 向上查找最近的 Suspense 边界,显示 fallback
  4. 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

Suspensefallback 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);
    },
  });
});

流式渲染的工作流程:

  1. 服务端先发送 Shell HTML(已就绪部分 + Suspense fallback)
  2. 各 Suspense 边界内的数据就绪后,通过流发送对应 HTML 替换 fallback
  3. 客户端选择性水合——用户交互的区域优先

八、总结

概念说明
React.lazy组件级代码分割,按需加载
Suspense声明式的加载状态管理
路由分割最常见的分割点
组件分割大型组件按需加载
嵌套 Suspense细粒度的加载状态控制
数据获取React 18+ 支持 Suspense 驱动的数据加载
Error Boundary搭配 Suspense 处理加载失败
SSR 流式渲染服务端 Suspense 支持流式 HTML 和选择性水合

Suspense 和 React.lazy 代表了 React 对异步操作的声明式抽象——开发者只需声明"等待什么"和"等待时显示什么",React 负责处理加载、渲染和状态切换的全部细节。