nextjs学习5:Suspense 与 Streaming

Suspense 是 Next.js 项目中常用的一个组件,了解其原理和背景有助于我们正确使用 Suspense 组件。

传统 SSR

传统 SSR,需要经过一系列的步骤,用户才能查看页面、与之交互。

具体这些步骤是:

  1. 服务端获取所有数据;
  2. 服务端渲染 HTML;
  3. 将页面的 HTML、CSS、JavaScript 发送到客户端;
  4. 使用 HTML 和 CSS 生成不可交互的用户界面(non-interactive UI);
  5. React 对用户界面进行水合(hydrate),使其可交互(interactive UI);

这些步骤是连续的、阻塞的。这意味着服务端只能在获取所有数据后渲染 HTML,React 只能在下载了所有组件代码后才能进行水合:

传统 SSR 几个缺点如下:

  1. SSR 的数据获取必须在组件渲染之前
  2. 组件的 JavaScript 必须先加载到客户端,才能开始水合
  3. 所有组件必须先水合,然后才能跟其中任意一个组件交互

Suspense

为了解决这些问题,React 18 引入了 <Suspense> 组件。

<Suspense> 允许你推迟渲染某些内容,直到满足某些条件(例如数据加载完毕)。

你可以将动态组件包装在 Suspense 中,然后向其传递一个 fallback UI,以便在动态组件加载时显示。

如果数据请求缓慢,使用 Suspense 流式渲染该组件,不会影响页面其他部分的渲染,更不会阻塞整个页面

让我们来写一个例子,新建 app/dashboard/page.js,代码如下:

js 复制代码
import { Suspense } from 'react'

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function PostFeed() {
  await sleep(2000)
  return <h1>Hello PostFeed</h1>
}

async function Weather() {
  await sleep(8000)
  return <h1>Hello Weather</h1>
}

async function Recommend() {
  await sleep(5000)
  return <h1>Hello Recommend</h1>
}

export default function Dashboard() {
  return (
    <section style={{padding: '20px'}}>
      <Suspense fallback={<p>Loading PostFeed Component</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading Weather Component</p>}>
        <Weather />
      </Suspense>
      <Suspense fallback={<p>Loading Recommend Component</p>}>
        <Recommend />
      </Suspense>
    </section>
  )
}

在这个例子中,我们用 Suspense 包装了三个组件,并通过 sleep 函数模拟了数据请求耗费的时长。加载效果如下:

让我们观察下 dashboard 这个 HTML 文件的加载情况,你会发现它一开始是 2.03s,然后变成了 5.03s,最后变成了 8.04s,这不就正是我们设置的 sleep 时间吗?

查看 dashboard 请求的响应头:

Transfer-Encoding 标头的值为 chunked,表示数据将以一系列分块的形式进行发送

分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许 HTTP 由网页服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在 HTTP 协议1.1版本(HTTP/1.1)中提供。

再查看 dashboard 返回的数据(这里我们做了简化):

js 复制代码
<!DOCTYPE html>
<html lang="en">
    <head>
        // ...
    </head>
    <body class="__className_aaf875">
        <section style="padding:20px">
            <!--$?-->
            <template id="B:0"></template>
            <p>Loading PostFeed Component</p>
            <!--/$-->
            <!--$?-->
            <template id="B:1"></template>
            <p>Loading Weather Component</p>
            <!--/$-->
            <!--$?-->
            <template id="B:2"></template>
            <p>Loading Recommend Component</p>
            <!--/$-->
        </section>
        // ...
        <div hidden id="S:0">
            <h1>Hello PostFeed</h1>
        </div>
        <script>
            // 交换位置
            $RC = function(b, c, e) {
                // ...
            };
            $RC("B:0", "S:0")
        </script>
        <div hidden id="S:2">
            <h1>Hello Recommend</h1>
        </div>
        <script>
            $RC("B:2", "S:2")
        </script>
        <div hidden id="S:1">
            <h1>Hello Weather</h1>
        </div>
        <script>
            $RC("B:1", "S:1")
        </script>
    </body>
</html>

可以看到使用 Suspense 组件的 fallback UI 和渲染后的内容都会出现在该 HTML 文件中,说明该请求持续与服务端保持连接,服务端在组件渲染完后会将渲染后的内容追加传给客户端

客户端收到新的内容后进行解析,执行类似于 $RC("B:2", "S:2")这样的函数交换 DOM 内容,使 fallback UI 替换为渲染后的内容。

这个过程被称之为 Streaming Server Rendering(流式渲染)它解决了上节说的传统 SSR 的第一个问题,那就是数据获取必须在组件渲染之前。使用 Suspense,先渲染 Fallback UI,等数据返回再渲染具体的组件内容。

使用 Suspense 还有一个好处就是 Selective Hydration(选择性水合)。简单的来说,当多个组件等待水合的时候,React 可以根据用户交互决定组件水合的优先级。比如 Sidebar 和 MainContent 组件都在等待水合,快要到 Sidebar 了,但此时用户点击了 MainContent 组件,React 会在单击事件的捕获阶段同步水合 MainContent 组件以保证立即响应,Sidebar 稍后水合。

Streaming

Suspense 背后的这种技术称之为 Streaming

将页面的 HTML 拆分成多个 chunks,然后逐步将这些块从服务端发送到客户端。

简单理解:

  • 核心逻辑 :服务器在生成 HTML 时,不用等所有数据(比如接口请求、数据库查询)都返回,而是先把页面的静态骨架(导航、页脚、空的内容容器)以 chunk 块的形式发给浏览器,浏览器收到后立刻渲染出骨架,减少白屏时间。
  • 之后服务器再逐个获取动态数据(比如商品列表、文章内容),每拿到一部分就生成对应的 HTML 片段,再以 chunk 块发送给浏览器,浏览器接收到后增量插入到页面中 ,页面内容逐步 填充 完成。
  • 技术依赖 :就是我们之前说的 HTTP 分块传输(Transfer-Encoding: chunked)或 HTTP/2 二进制分帧,Next.js、Nuxt.js 等框架的 SSR 流式渲染都是这么实现的。

这样就可以更快的展现出页面的某些内容,而无需在渲染 UI 之前等待加载所有数据。

提前发送的组件可以提前开始水合,这样当其他部分还在加载的时候,用户可以和已完成水合的组件进行交互,有效改善用户体验

传统 SSR:

使用 Streaming 后:

在 Next.js 中有两种实现 Streaming 的方法

  1. 页面级别,使用 loading.jsx
  2. 特定组件,使用 <Suspense>

Suspense 和 Streaming 确实很好,将原本只能先获取数据、再渲染水合的传统 SSR 改为渐进式渲染水合

但还有一些问题没有解决:

1. 用户下载的 JavaScript 代码,该下载的代码还是没有少,可是用户真的需要下载那么多的 Javascript 代码吗?

2. 所有的组件都必须在客户端进行水合,对于不需要交互性的组件其实没有必要进行水合。

要解决这个问题,就是服务端组件要解决的问题了。

相关推荐
C_心欲无痕5 天前
Next.js 的默认开发快速构建工具Turbopack
javascript·devops·next.js·turbopack
C_心欲无痕5 天前
Next.js Script 组件详解
开发语言·javascript·ecmascript·next.js
RedHeartWWW11 天前
初识next-auth,和在实际应用中的几个基本场景(本文以v5为例,v4和v5的差别主要是在个别显式配置和api,有兴趣的同学可以看官网教程学习)
前端·next.js
小满zs13 天前
Next.js第二十一章(环境变量)
前端·next.js
小p14 天前
nextjs学习9:数据获取fetch、缓存与重新验证
next.js
陈佬昔没带相机14 天前
2025年终总结:Vibe Coding 之后,胆儿肥了
ai编程·全栈·next.js
小p16 天前
nextjs学习8:静态渲染、动态渲染、Streaming渲染
next.js
小满zs20 天前
Next.js第二十章(MDX)
前端·next.js
小p21 天前
nextjs学习6:服务端组件和客户端组件
next.js
多啦C梦a22 天前
《双Token机制?》Next.js 双 Token 登录与无感刷新实战教程
前端·全栈·next.js