6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

この記事の背景、目的

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

今回作ったコード

Next.js 13:

比較対象SPA(Vite&React)

環境構築

yarn create next-app

appディレクトリを有効化

next.config.js
experimental: {
    appDir: true
}

最初のページの実装

Next.js 13のapp/配下では従来のNext.jsのルーティングとは異なり、page.tsxのみがページとして扱われます。

まずは、Hello World的なページを作ってみます。
image.png

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

実際にデプロイして確認してみると下の画像のようにレスポンスのHTMLに直接page内のElementが埋め込まれています。
つまり、ブラウザ上でのHydrationは行われません。
image.png

APIからのデータ取得

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

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

Next.js 13のapp/配下ではpageのコンポーネントを非同期関数として実装することができます。
この非同期関数が完了するまではloading.tsxかSuspenceのfallbackが表示されます。
image.png

SPAとの比較

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

image.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' })
  
...

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

image.png

挙動の確認

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

Static Server Component

3秒の遅延は見られません
image.png

Dynamic Server Component

3秒以上遅延しますが、LayoutとLoading コンポーネントが先に表示されます。
image.png

SPA

image.png

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

Streamingの挙動

Dynamic Server Componentを利用する場合、データ取得開始時にはLayout+Loadingのコンポーネントのみを送っています。
こちらは、データが表示された後でもPreviewから確認できます。
image.png

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

image.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

検証には自分で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クライアントがあると嬉しいなあという感想を持ちました。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?