banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

為什麼是 RSC (二)

漸進式渲染#

Important

漸進式渲染,或者稱作流式渲染。這不是一個只能在 RSC 中可以享受到的特徵,這種渲染模式和 SuspenserenderToPipeableStreamrenderToReadableStream 有關。

但是在 Next.js 中你需要使用 App router RSC 才能享受此特徵。所以本節討論 Next.js 的渲染模式。

由於 RSC 組件支持異步,所以組件和組件平行關係之間的渲染並沒有相互依賴性,並且可被拆分。多個組件可以誰先兌現誰先渲染。這在組件之間分別獲取不同數據時非常好用。

例如一個頁面上,存在兩個組件,A 組件獲取商品列表並渲染輸出,B 組件獲取商品分類並輸出。兩者都是獨立的邏輯。

在傳統 SSR 模式中,頁面中組件的數據需要從頁面頂層獲取向下傳遞到組件,這樣就會導致 A,B 組件的渲染都要等待頁面數據獲取完才能開始渲染。

假設我們的接口為:

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const fetchGoods = async () => {
  await sleep(1000)
  return [
    {
      name: 'iPhone 15',
      variants: ['Blue'],
    },
    {
      name: 'iPad Pro',
      variant: 'Black',
    },
  ]
}
const fetchCategories = async () => {
  await sleep(3000)
  return ['Electronics', 'Books']
}
export default (props: {
  goods: { name: string; variants: string[] }[]
  categories: string[]
}) => {
  return (
    <div>
      <h1>商品</h1>
      <ul>
        {props.goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
      <h1>分類</h1>
      <ul>
        {props.categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

export const getServerSideProps = async () => {
  const [goods, categories] = await Promise.all([
    fetchGoods(),
    fetchCategories(),
  ])
  return {
    props: {
      goods,
      categories,
    },
  }
}

上面的例子中,伺服器響應瀏覽器至少需要 3s,之後才能在瀏覽器呈現數據。

如果在 RSC 中,兩者之間可以誰先完成誰先渲染。


export default () => {
  return (
    <>
      <Suspense>
        <Goods />
      </Suspense>

      <hr className="my-8 h-1 bg-gray-100" />
      <Suspense>
        <Categories />
      </Suspense>
    </>
  )
}

const Goods = async () => {
  const goods = await fetchGoods()
  return (
    <div>
      <h1>商品</h1>
      <ul>
        {goods.map((good) => (
          <li key={good.name}>
            {good.name} - {good.variants.join(', ')}
          </li>
        ))}
      </ul>
    </div>
  )
}
const Categories = async () => {
  const categories = await fetchCategories()
  return (
    <div>
      <h1>分類</h1>
      <ul>
        {categories.map((category) => (
          <li key={category}>{category}</li>
        ))}
      </ul>
    </div>
  )
}

image

可以看到,等待 1s 後首先渲染出了商品,然後 2s 之後渲染出分類。這便是漸進式渲染的好處,最大程度提升了 First Meaningful Paint (FMP) 和 Largest Contentful Paint (LCP)。

Important

這種渲染方式雖然提升了首屏的性能,但是因為這個特徵也會讓頁面佈局的抖動更加明顯,在開發過程中應該更加需要注意這點,盡量在 Supsense fallback 中填充一個和原始組件大小相同的佔位。

靈活的伺服器數據獲取#

由於 RSC 中可以使用任何 Nodejs 方法,所以在數據獲取上異常方便,我們不必單獨編寫一個 Server Api,然後只在 Client 去請求 API,也不必在 SSR 中請求接口,把數據水合到 Client 組件中。我們只需要編寫到伺服器獲取數據的方法,然後直接在 RSC 中調用。

例如,我們現在做一個伺服器的管理,其中有組件需要服務的狀態信息。

在 RSC 之前我們一般這樣去獲取伺服器的狀態信息。

首先,在 SSR 時,使用 getServerSideProps 調用 getServerStatus() 把數據返回,然後在 Page 中接收這個 props。如果需要定時去刷新這個狀態的話,我們還需要編寫一個 API 接口包裝這個方法,在 RCC 中輪詢。

https://nextjs-book.innei.in/draw/12.server-status-in-ssr-and-csr.json

在 RSC 中,我們直接調用並渲染,然後使用 revalidatePath() 去做數據刷新,無需編寫任何 API。

export default function Page() {
  return (
    <div className="flex gap-4">
      <ServerStatus />
      <Revalidate />
    </div>
  )
}

const ServerStatus = async () => {
  // 獲取伺服器狀態
  const status = await getServerStatus()

  return <>{/* 渲染 */}</>
}
'use server'
export const revalidateStatus = async () => {
  revalidatePath('/status')
}

'use client'

export const Revalidate = () => {
  useEffect(() => {
    const timerId = setInterval(() => {
        revalidateStatus()
      }, 5000)
    return () => {
      clearInterval(timerId)
    }
  }, [])
  return null
}

這裡的 Revalidate 組件 + revalidateStatus 就是利用了 Server Action 的特徵去做了頁面的數據更新。

看似這裡需要寫三個文件,又要區分 RSC 和 RCC 好像挺複雜的,但是比起另寫 API 和還有手動寫 API 的類型定義並且無法做到 End-to-End type safe 的割裂感還是好太多了。

Server Action 的優勢#

上一節其實已經利用了 Server Action 完成了頁面的數據更新,其實 Server Action 還有其他的用法。

我們知道 Server Action 其實一個 POST 請求,我們編寫一個異步的方法,並且標記為 'use server',那麼在 RCC 中調用這個方法時,會自動幫你完成:向伺服器發送 POST 請求獲取這個方法的響應數據,這個數據可以是流式的,並且在此方法中可以調用 revalidate 等方法去觸發頁面的更新。

文檔中一般會告訴你 Server action 來處理表單的提交,觸發對數據的更新,最後反應到 UI 上。

不僅如此,其實我們可以利用 Server Action 去獲取數據。

還是以上面的例子為例,只不過這次我們全部在 RCC 中實現數據獲取和輪詢。

```tsx filename="app/server-action/page.tsx" {5,8} 'use client'

import useSWR from 'swr'

import { getLoadAvg } from './action'

export default function Page() {
const { data } = useSWR('get-load-avg', getLoadAvg, {
refreshInterval: 1000,
})
return <> 負載平均: {data?.loadavg.join (', ')}</>
}

</Tab>
  <Tab label="app/server-action/action.tsx">
```tsx filename="app/server-action/action.tsx" {1}
'use server'

import os from 'os'

export const getLoadAvg = async () => {
  const avg_load = os.loadavg()

  return {
    loadavg: avg_load,
  }
}

這樣不僅少寫了一個 API 接口,同時這樣的寫法也做到了 End-to-End type safe。

另外,推薦閱讀:Server Action & Streamable UI


為什麼是 RSC - 聊點不一樣的 Next.js

此文由 Mix Space 同步更新至 xLog
原始鏈接為 https://innei.in/posts/tech/why-react-server-component-2


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。