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

profile image

进一步了解悬念是如何产生的,它在实践中是如何运作的,以及在哪些情况下可行,哪些情况下不可行。

本帖由 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> 封装您不想等待所有数据加载的组件,并将您想在加载时显示的组件放在fallback props 中即可。

javascript
function BlogPost() {
  return (
    <div>
      <Nav />
      <Sidebar />
      <Article />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  );
}

Image.png

初始 HTML 页面如下所示

html
<main>
  <!-- 생략... -->
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

之后,一旦服务器上的注释组件数据准备就绪,它就会经历以下过程

html
<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

Image.png

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

改进水合

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

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

function BlogPost() {
  return (
    <div>
      <Nav />
      <Article />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  );
}

Image.png

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

Image.png

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

AnimatedImage.gif

可触发暂停的条件

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

  1. use 使用钩子获取数据

    javascript
    import { use } from 'react';
    
    // 데이터를 가져오는 함수
    function fetchUserData(userId) {
      return fetch(`/api/users/${userId}`)
        .then(res => res.json());
    }
    
    function UserProfile({ userId }) {
      const user = use(fetchUserData(userId));
      return (
        <div>
          <h1>{user.name}</h1>
          <p>{user.bio}</p>
        </div>
      );
    }
    
    // Suspense로 래핑
    function App() {
      return (
        <Suspense fallback={<Loading />}>
          <UserProfile userId={1} />
        </Suspense>
      );
    }
  2. 懒惰组件

    javascript
    import { lazy, Suspense } from 'react';
    
    // 동적으로 로드되는 컴포넌트
    const HeavyComponent = lazy(() => import('./HeavyComponent'));
    
    function App() {
      return (
        <div>
          <h1>My App</h1>
          <Suspense fallback={<Loading />}>
            <HeavyComponent />
          </Suspense>
        </div>
      );
    }
  3. 服务器组件中的异步

    javascript
    // 서버 컴포넌트
    async function BlogPost({ id }) {
      const post = await fetchBlogPost(id);
      return (
        <article>
          <h1>{post.title}</h1>
          <p>{post.content}</p>
        </article>
      );
    }
    
    function BlogPage({ id }) {
      return (
        <Suspense fallback={<LoadingPost />}>
          <BlogPost id={id} />
        </Suspense>
      );
    }
  4. useDeferredValue 在服务器组件中

    javascript
    function SearchResults({ query }) {
      // 검색어 변경을 지연시켜 처리
      const deferredQuery = useDeferredValue(query);
    
      return (
        <Suspense fallback={<Loading />}>
          <SearchResultsList query={deferredQuery} />
        </Suspense>
      );
    }

不触发暂停时

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

  1. useEffect 从内部修补数据

    javascript
    // ❌ Suspense를 발동시키지 않음
    function UserProfile({ userId }) {
      const [user, setUser] = useState(null);
    
      useEffect(() => {
        fetchUserData(userId).then(setUser);
      }, [userId]);
    
      if (!user) return <Loading />;
      return <h1>{user.name}</h1>;
    }
  2. 通用承诺处理

    javascript
    // ❌ Suspense를 발동시키지 않음
    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 />;
      return <h1>{user.name}</h1>;
    }

使用 Tanstack 查询时

必须使用useSuspenseQuery ,而不是之前使用的useQuery


另请参见

❤️ 0
🔥 0
😎 0
⭐️ 0
🆒 0