了解什么是 React Suspense 以及如何使用它

🌐

本帖由 DeepL 翻译。如有任何翻译错误,请告知我们!

服务器端渲染(SSR)的局限性

服务器端渲染(SSR)在初始加载性能和搜索引擎优化方面有很大的优势,但它也有一些明显的局限性。在本文中,让我们来看看传统 SSR 所存在的问题,并了解 React 18 中为解决这些问题而引入的 Suspense。

1.数据获取的阻塞问题

SSR 最大的问题之一就是需要一次性从服务器获取所有数据。在将组件树呈现为 HTML 之前,我们需要获得该页面所需的所有数据。

例如,博文页面由以下组件组成

  • 导航栏
  • 侧边栏
  • 帖子内容
  • 评论部分

如果获取评论数据需要很长时间,开发人员有以下选择

  1. 将评论排除在服务器渲染之外

    • 优点:可以快速显示其他组件
    • 缺点:在加载 JavaScript 之前,用户无法看到注释
  2. 渲染带有注释的服务器

    • 优点:评论包含在初始 HTML 中
    • 缺点:在等待注释数据时,整个页面的渲染会延迟

2.水合问题

SSR 的第二个大问题与水合过程有关。为了将服务器上生成的 HTML 与客户端上的组件树相匹配,React 必须等待所有组件的 JavaScript 代码加载完毕。

React 的水合过程也存在问题。目前,React 会一次性水合整个组件树,这会导致以下问题

  1. 在整个组件树水合之前,您无法与任何组件交互

  2. 性能问题,尤其是在低端设备上

    • 如果 JavaScript 逻辑较多,屏幕会冻结
    • 用户即使想导航到另一个页面,也必须等待水合完成

3 瀑布流程的局限性

这些问题的根本原因在于 SSR 遵循以下顺序的 "瀑布式 "流程

  1. 获取数据(服务器)
  2. HTML 渲染(服务器)
  3. JavaScript 代码加载(客户端)
  4. 水合(客户端)

每一步只有在前一步完全完成后才能开始,从而降低了整体性能和用户体验。

新解决方案 "悬念

为了解决这些问题,React 18 引入了 "暂停 "功能。有了 Suspense,应用程序的不同部分可以分别加载和水合,从而有效解决上述问题。

如何使用

使用 Suspense 非常简单。只需用<Suspense>封装任何您不想等待其所有数据加载的组件,并将您希望在加载时显示的组件放入回退道具中即可。

函数 BlogPost() {
  返回 (
    <div> <Nav
      <Nav /> <Sidebar
      <Sidebar /> <Article
      <Article /> <Article
      <悬念 fallback={<CommentsSkeleton />}>
        <评论 />
      </悬念
    </div> </div>
  );
}

Image.png

初始 HTML 将如下所示

<main> <!
  <省略...-->
  <section id="comments-spinner">
    <!
    <img width=400 src="spinner.gif" alt="加载中..." /> <!
  </section
</main>

之后,当评论组件的数据在服务器上准备就绪时,我们将这样做

<div hidden id="comments">
  <!
  <p> 第一条评论</p
  <p>第二条评论</p
</div> </div>
<script
  // 此实现略有简化
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script> </script>

Image.png

这样,我们就不必为了在屏幕上显示某些内容而获取所有数据了。

改进水合

我们已经创建了初始 HTML,但在开始水合之前,我们仍需要加载所有 JavaScript。在这种情况下,我们可以使用代码分割和lazy来隔离水合过程。

const Comments = lazy(() => import('./Comments'));

函数 BlogPost() {
  返回
    <div> <div
      <Nav /> <Article
      <Article />.
      <悬念 fallback={<CommentsSkeleton />}>
        <评论 /> <悬念
      </悬念
    </div> </div>
  );
}

Image.png

除注释部分外,其余 JavaScript 加载完成后,水合将开始。稍后,当加载完评论组件中的所有 JavaScript 后,评论也将水合。

Image.png

下面是到目前为止的流程概览

AnimatedImage.gif

可触发暂停的条件

并不是每一种数据加载都会触发暂停,那么哪些数据源与暂停兼容呢?

  1. 使用use钩子获取数据
import { use } from 'react'// 获取数据的函数
function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(res => res.json());
}

function UserProfile({ userId }) {
  const user = use(fetchUserData(userId));
  返回 (
    <div> <h1>{user.name}</h1
      <h1>{user.name}</h1> <p>{user.bio}</p
      <p>{user.bio}</p>
    </div> </div
  );
}

// 用悬念进行包装
function App() {
  返回 (
    <Suspense fallback={<Loading />}>
      <UserProfile userId={1} /> </Suspense
    </Suspense> </Suspense
  );
}
  1. 懒惰组件
import { lazy, Suspense } from 'react'// 动态加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  返回 (
    <div> <div>我的应用程序</div
      <h1>我的应用程序</h1
      <悬念 fallback={<Loading />}>
        <HeavyComponent />
      </Suspense
    </div> </div>
  );
}
  1. // 服务器组件的异步
// 服务器组件
async 函数 BlogPost({ id }) {
  const post = await fetchBlogPost(id);
  返回 (
    <文章
      <h1>{post.title}</h1> <p>{post.content}</p> <article
      <p>{post.content}</p>
    </article> </article
  );
}

function BlogPage({ id }) {
  返回 (
    <悬念 fallback={<LoadingPost />}>
      <BlogPost id={id} />
    </Suspense> <Suspense
  );
}
  1. 使用延迟值
function SearchResults({ query }) {
  // 推迟对搜索查询的更改
  const deferredQuery = useDeferredValue(query);

  返回 (
    <Suspense fallback={<Loading />}>
      <搜索结果列表 query={deferredQuery} />
    </Suspense
  );
}

不触发暂停时

另一方面,以下常见的数据获取模式不会触发暂停状态

  1. useEffect内的数据修补
// ❌ 不会触发暂停状态
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData(userId).then(setUser);
  }, [userId]);

  if (!user) 返回 <Loading />;
  返回 <h1>{user.name}</h1>;
}
  1. 正常 Promise 处理。
// ❌ 不会触发暂停
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  Promise.resolve(fetchUserData(userId))
    .then(setUser)
    .catch(setError);

  if (error) return <Error error={error} />if (!user) return <Loading />;
  返回 <h1>{user.name}</h1>;
}

使用 Tanstack 查询时

时,请确保使用 useSuspenseQuery而不是useQuery

请参见