Next.js 13を実際に動かしてデプロイしてみた

## この記事の背景、目的
前回の投稿では、Next.js 13のドキュメントのServerComponent関連を一通り見てみたので
今回は実際に手元で触って、デプロイし、挙動を確認したいとおもいます。
https://qiita.com/mjm2kt/items/82bc7cd346701937f4f9

## 今回作ったコード
Next.js 13:

https://github.com/macoto1995/next-13

比較対象SPA（Vite＆React）

https://github.com/macoto1995/vite-react-ts

## 環境構築
```shell
yarn create next-app
```

### appディレクトリを有効化
```next.config.js
experimental: {
    appDir: true
}
```
### 最初のページの実装
Next.js 13のapp/配下では従来のNext.jsのルーティングとは異なり、page.tsxのみがページとして扱われます。

まずは、Hello World的なページを作ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/c1a68171-f77b-6e4e-48d4-9a95dfa2585b.png)

Next.js 13のapp/配下ではデフォルトでServer Componentとして扱われるように設定されています。

実際にデプロイして確認してみると下の画像のようにレスポンスのHTMLに直接page内のElementが埋め込まれています。
つまり、ブラウザ上でのHydrationは行われません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/4896712c-0498-e9f0-d37a-3914f872676e.png)

## APIからのデータ取得
話題のfetch APIを試すためにPokeAPIからデータを取得＆表示するように実装していきます。

https://beta.nextjs.org/docs/data-fetching/fetching

https://pokeapi.co/

```app/pokemon/page.tsx
type PokemonListResponse = {
  count: number
  next: string
  results: {
    name: string
    url: string
  }[]
}
const getPokemonList = async () => {
  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100&offset=0')
  const json = await res.json() as PokemonListResponse
  const pokemons = json.results.map(pokemon => {
    const id = pokemon.url.split('/').at(-2)
    const imgSrc = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`
    const url = `/pokemon/${id}`
    return({...pokemon, id, url, imgSrc })
  })
  return pokemons
}

export default async function PokemonIndexPage() {
  const pokemons = await getPokemonList()
  return (
    <>
      {
        pokemons.map(pokemon => (
          <div key={pokemon.id}>
            <h4>{pokemon.name}</h4> 
            <a href={pokemon.url}>{pokemon.name}</a>
            <img alt={pokemon.name} src={pokemon.imgSrc} />
          </div>
        ))
      }  
    </>
  )
   
}
```

先ほど同様レスポンスのHTMLに表示されているDOMがそのまま含まれています。
また、APIから取得したデータもここに含まれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/6c75181a-77aa-2ade-133f-48b1d9b71dff.png)

Next.js 13のapp/配下ではpageのコンポーネントを非同期関数として実装することができます。
この非同期関数が完了するまではloading.tsxかSuspenceのfallbackが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/6ed57d27-5658-7f35-2a12-f6d0e0e5e1ec.png)

#### SPAとの比較
同様のページをSPA（Vite＋React）で実装したものと比較してみます。
仮想DOMなので、HTMLのbodyはdivが一つのみです。
また、Javascriptがロードされるまでfetchは開始しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/39b85358-edd2-9920-ec9d-a2ad53ba7126.png)

#### Static Server ComponentとDynamic Server Componentの比較

Next13の実装をcacheあり、なしで2つのページにしてみます。
デフォルトでキャッシュあり（更新しない）になっているのでfetchのオプションに`cache: 'no-store'`をつけて、リクエストごとにfetchが実行されるように変更します。

```app/pokemon-static/page.tsx
...
const getPokemonList = async () => {
// デフォルトでキャッシュされる
  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100&offset=0')
  
...

```


```app/pokemon-dynamic/page.tsx
...
const getPokemonList = async () => {
// cache: 'no-store' をつけると毎回実行される
  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100&offset=0', { cache: 'no-store' })
  
...

```
https://github.com/macoto1995/next-13/commit/d8daefa26e0c5dfadc5f7aa7846a03c20940d572

ビルド時にもfetchは行われるためフェッチするサイズが大きいとbuildの時間が長くなります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/0cc2700b-6099-d6be-4560-4246f8eaf173.png)

#### 挙動の確認
※ PokeAPIがレスポンス早すぎて差分が見えないためfetchの前に3秒の遅延を挿入しています
https://github.com/macoto1995/next-13/commit/79602ab3321a537d4890c18b731fc45cbac5dc62#diff-ae2e62e1752aea364291e8674b1b68a52aaee0feccc456a82e249f7b70f47a4aR11

##### Static Server Component
3秒の遅延は見られません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/b44b7a78-f618-074e-4d7f-f0da8063407b.png)

##### Dynamic Server Component

3秒以上遅延しますが、LayoutとLoading コンポーネントが先に表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/fe60e1e5-32e7-bd2e-1728-0b521f9f8999.png)

##### SPA
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/74da5553-ecdb-15f8-4da5-c914495ce227.png)

index.xxxxxx.jsがロードされた後Loadingコンポーネントが表示されます。
また、JSがロードされるまでは真っ白の画面しか表示されません。

### Streamingの挙動
Dynamic Server Componentを利用する場合、データ取得開始時にはLayout＋Loadingのコンポーネントのみを送っています。
こちらは、データが表示された後でもPreviewから確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/94e9c3a9-4d49-4105-9e6a-5a9de5aa6c25.png)

また、HTMLのダウンロード完了後は取得したデータを含んだDOMになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/11afa7ad-41f4-6422-6f6c-b1431e3b122e.png)





## 今回検証できなかったこと
### Parallel Fetch
複数の非同期でのデータ取得を一つのページで行う場合、並列で実行し、Suspenceで囲うように公式のドキュメントでは説明されています。
これがどのような挙動になるか確認してみます。

```app/pokemon-dynamic/page.tsx
import Link from "next/link"
import { Suspense } from "react"

type PokemonResponse = {
  id: string
  name: string
}

type PokemonListResponse = {
  count: number
  next: string
  results: {
    name: string
    url: string
  }[]
}


const getFavoritPokemon = async () => {
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
  await sleep(1000)
  const res = await fetch('https://pokeapi.co/api/v2/pokemon/pikachu', { cache: 'no-store' })
  const { id, name, } = await res.json() as unknown as PokemonResponse


  const imgSrc = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`
  const url = `/pokemon/${id}`

  return { name, id, imgSrc, url}
}

const getPokemonList = async () => {
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
  await sleep(3000)
  
  const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100&offset=0', { cache: 'no-store' })
  const json = await res.json() as PokemonListResponse
  
  const pokemons = json.results.map(pokemon => {
    const id = pokemon.url.split('/').at(-2)
    const imgSrc = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`
    const url = `/pokemon/${id}`
    return({...pokemon, id, url, imgSrc })
  })
  return pokemons
}

export default async function PokemonIndexPage() {
  const favoritPokemon = await getFavoritPokemon()
  const pokemons = await getPokemonList()
  return (
    <>
      <h3>Your Favorit Pokemon</h3>
      <Suspense fallback={<p>Loading Your Favorit Pokemon...</p>}>
        <h4>{favoritPokemon.name}</h4>
        <Link href={favoritPokemon.url}>{favoritPokemon.name}</Link>
        <img alt={favoritPokemon.name} src={favoritPokemon.imgSrc} />

      </Suspense>

      <h3>All Pokemons List</h3>
      <Suspense fallback={<p>Loading Pokemons...</p>}>
        {
          pokemons.map(pokemon => (
            <div key={pokemon.id}>
              <h4>{pokemon.name}</h4> 
              <Link href={pokemon.url}>{pokemon.name}</Link>
              <img alt={pokemon.name} src={pokemon.imgSrc} />
            </div>
          ))
        }  
      </Suspense>
    </>
  )
   
}
```
こちらはデータの取得した順に表示してくれるのかとおもいましたが、両方が完了してからデータが表示されました。
また、Vercelにデプロイしたものでは必ずタイムアウトしてしまったのでローカルでの挙動確認のみ行っています。
よくよく考えると、今回はsetTimeoutでプロセスを占拠しているため、実際のAPIリクエストでは並列で処理が行われるかもしれません。

公式の下の図のように非同期処理が終わった順でレンダリングされることを期待していましたが、今回はそこまで検証できませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181998/4afc515a-abca-313a-24e8-88a747b15684.png)

https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense

検証には自分でAPIを立てて遅延をコントロールした状態で必要がありそうです。

### その他
chakra-uiなどいくつかの人気のUIライブラリはServer Componentに対応していないようです。
Next.js 13がデファクトスタンダードになったら、この辺りを今後の技術選定では気にしていかなければいけないでしょう。
https://chakra-ui.com/getting-started/nextjs-guide

## 所感
低速回線や低スペックハードウェアを使っているユーザーに対してはやはり、HTMLのみが送られるというのはメリットが大きいです。
ReactのSuspenceの良さをフルで活かすならServer Componentの利用は必須かとおもいます。

ただ、既存のCSRの仕組みとは大きく異なるためUIライブラリやデータ取得で、ベストプラクティスを新たに積み上げていく必要性も感じました。
また、GraphQLをサポートしていないのでServer Component用のGraphQLクライアントがあると嬉しいなあという感想を持ちました。