Skip to content

NextJs 组件 ​

服务端组件 vs 客户端组件 ​

Next.js 中默认是服务端组件,使用 'use client' 声明客户端组件。

服务端组件 ​

tsx
// 默认就是服务端组件,无需声明
export default async function Address() {
  const res = await fetch('https://api.example.com/posts')
  const data = (await res.json()).slice(0, 10)

  return (
    <ul>
      {data.map(({ title, id }) => (
        <li key={id}>{title}</li>
      ))}
    </ul>
  )
}
// 默认就是服务端组件,无需声明
export default async function Address() {
  const res = await fetch('https://api.example.com/posts')
  const data = (await res.json()).slice(0, 10)

  return (
    <ul>
      {data.map(({ title, id }) => (
        <li key={id}>{title}</li>
      ))}
    </ul>
  )
}
  • 数据获取在服务端执行
  • 不会打包到客户端 bundle 中(代码更小)
  • 不支持 useState、useEffect 等交互 API
  • 可直接 async/await

客户端组件 ​

tsx
'use client'
import { useEffect, useState } from 'react'

export default function Address() {
  const [list, setList] = useState([])

  useEffect(() => {
    fetch('https://api.example.com/todos')
      .then(res => res.json())
      .then(setList)
  }, [])

  return <ul>{list.map(item => <li key={item.id}>{item.title}</li>)}</ul>
}
'use client'
import { useEffect, useState } from 'react'

export default function Address() {
  const [list, setList] = useState([])

  useEffect(() => {
    fetch('https://api.example.com/todos')
      .then(res => res.json())
      .then(setList)
  }, [])

  return <ul>{list.map(item => <li key={item.id}>{item.title}</li>)}</ul>
}
  • 支持交互 API 和浏览器 API
  • 代码会打包到客户端 bundle 中

对比 ​

服务端组件客户端组件
数据获取更快(服务端直连)需要客户端请求
安全性逻辑不暴露给前端代码对前端可见
缓存渲染结果可缓存无
Bundle不会打包到客户端会打包到客户端
FCP更短较长
交互不支持(无 state/effect)完整支持 React 交互
运行时机构建时 + 服务端构建时 + 服务端 + 客户端

水合(Hydration) ​

SSR 将组件渲染为 HTML,但 HTML 没有交互性。客户端下载 JS 后为 HTML 赋予交互性的过程称为水合。

传统 SSR 的问题:

  • 数据的获取必须在组件渲染之前
  • 所有组件的 JS 必须先加载到客户端才能开始水合
  • 所有组件都必须水合完毕才能交互

Suspense 与 Streaming ​

Suspense 允许推迟渲染部分内容,直到数据加载完毕:

tsx
import { Suspense } from 'react'

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

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

async function Component2() {
  await sleep(3000)
  return <h1>Hello Component2</h1>
}

export default function Page() {
  return (
    <section>
      <Suspense fallback={<p>Loading Component1</p>}>
        <Component1 />
      </Suspense>
      <Suspense fallback={<p>Loading Component2</p>}>
        <Component2 />
      </Suspense>
    </section>
  )
}
import { Suspense } from 'react'

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

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

async function Component2() {
  await sleep(3000)
  return <h1>Hello Component2</h1>
}

export default function Page() {
  return (
    <section>
      <Suspense fallback={<p>Loading Component1</p>}>
        <Component1 />
      </Suspense>
      <Suspense fallback={<p>Loading Component2</p>}>
        <Component2 />
      </Suspense>
    </section>
  )
}

Streaming 是 Suspense 背后的实现技术,将 HTML 拆分为多个 chunk 逐步发送:

  • 提前发送的组件可提前水合
  • 减少 TTFB、FCP、TTI

在 Next.js 中实现 Streaming 的两种方式:

  1. 组件级别:使用 Suspense
  2. 页面级别:使用 loading.tsx

RSC vs SSR ​

RSC(React Server Components)SSR(Server-Side Rendering)
重点组件级别的细粒度渲染控制页面级别的服务端渲染
代码打包组件代码不打包到 bundle所有依赖都打包到 bundle
数据获取组件中直接获取需通过 getServerSideProps
交互性只适用于静态内容水合后完整交互

RSC 解决了 SSR 的核心缺陷:用户需要下载的 JS 代码量不会减少。使用 RSC(服务端组件)可以将不必要的代码隐藏在服务端。