Open10

App Router と GraphQL を使いこなしたい

Hayato YokoyamaHayato Yokoyama

1回のfetchで過不足なくデータを取れるのが、GraphQLのメリット。
App RouterはサーバーコンポーネントをSuspenceで囲って、処理が完了したところから順次表示させたい思想がある。
1回で必要なデータを全部取りたいGraphQLと、コンポーネントごとにデータを取って読み込みが完了しているところから表示させたい(ストリーミングSSRしたい)App Roter が相反している。

Hayato YokoyamaHayato Yokoyama

App Router はデフォルトでキャッシュを保持し続ける仕様にもある通り、積極的にキャッシュを使うことを想定されているので、1回でページ全部のデータを取ってくるのが合わない気がする。

App Routerだと1ページのなかで細かくデータを取って、それぞれのデータ取得にあったキャッシュ設定を効かせることができる。

Hayato YokoyamaHayato Yokoyama

レスポンスの型やリクエスト用の関数や引数の型などをスキーマから生成できるのは、GraphQLのメリット。

Hayato YokoyamaHayato Yokoyama

https://speakerdeck.com/taro28/graphqlwoserver-componentsdeshi-itai

クエリの中で defer directive をつけると、deferをつけたデータのレスポンスを遅延して取得できるらしい。

でも、GraphQLってそもそも「1回で取得することができる」がメリットなのに、deferつけてデータ取得を分割したら本末転倒ではないか?
defer だと、1リクエストで複数レスポンスの形にできるから、複数回取得処理が実行されるわけではなさそう。

Hayato YokoyamaHayato Yokoyama

urql で以下のようなクエリ、フラグメントで poke API を叩いてみても、うまくいかない感じがした。
どうやらサーバー側でオプトインの設定を有効にする必要がありそう。

gql`
  query Pokemons {
    pokemons(first: 151) {
      id
      ...PokemonCard @defer
    }
  }
`;

gql`
  fragment PokemonCard on Pokemon {
    image
    name
    number
    types
  }
`;

LaravelのLighthouse だと以下のような設定が必要っぽい
https://lighthouse-php.com/6/performance/deferred.html#setup

Hayato YokoyamaHayato Yokoyama

GraphQL

フロント側でレスポンスの形を決定できる。
したがって、1回過不足なく必要なデータを取得できる。
ただ、親で1回でまとめて取得したものをそれぞれ子に渡す形になるため、子に修正が入ったときの改修範囲が大きくなる。

子の修正の影響を受けにくくするのが、Fragmentコロケーション。
子コンポーネントの中でコンポーネント自身が欲しいデータを定義(決定)させる。
子がどんなデータを欲しているかを、親は知らなくていい。でもそれぞれの子が欲しているデータをまとめて1回で取得できる。

Next.js(App Router) というよりは React

React18以降はSuspense for fetching の考え方が基本?

画面の初期表示までの時間を短くするために、データ取得などで表示が遅くなる子コンポーネントはローディングを表示させて、それ以外のコンポーネントをレンダリングする。
Suspenseをうまく使うと特定の重い処理がある子コンポーネントのせいでページ全体の表示が遅くなることを防ぐことができる。

Suspense for fetching がGraphQLの思想と合わないところ

GraphQLは1回のデータ取得で、過不足なくデータを取得できるのがメリット。逆に言えば、とある一部の重いデータ取得処理のせいでページ全体の表示が遅くなる可能性がある。

ReactのSuspense for fetchingの考え方だと、Suspenseで囲った重いデータ取得があるコンポーネントを遅らせて表示することでページ全体の表示を早くする。

GraphQLの1回でデータを取りたい思想と、Reactのデータの読み込みが完了したところから受け取って、コンポーネントを表示させたい思想が相反する。

GraphQLとApp Routerの両者のいいとこ取りをできそうな defer ディレクティブ

deferをつけたフラグメントはデータ取得を遅延させることができる。
遅延してデータを読み込むフラグメントを定義するコンポーネントはSuspenseを囲うことで、遅延している間はローディングを表示させることができる。

// 最初に返ってくるデータ
{
  "data": {
     レスポンスの中身
  },
  "hasNext": true // 「遅れて取得するデータがありますよ」のしるし
}
{
  "label": "ラベル名",
  "path": [どこのリクエストか?],
  "data": {
        遅れて取得したレスポンスデータ
    }
  },
  "hasNext": false // 「もう遅れて取得するデータはないよ」のしるし
}

こうすると、1リクエストで複数レスポンスの形にできるから、
1回のデータ取得処理で済むパフォーマンス面の良さと、データが帰ってきたところから表示する初回表示の速さを両立できそう。

defer を使える環境はまだ限定的。
defer を使うには、サーバー側でオプトインの設定を有効にする必要があるし、defer を使えるクライアントライブラリを選ぶ必要がある。(urqlは使うことができそう)

Hayato YokoyamaHayato Yokoyama

https://zenn.dev/akfm/books/nextjs-basic-principle/viewer/part_1_server_components#graphqlとの相性の悪さ

RSCにGraphQLを組み合わせることはメリットよりデメリットの方が多くなる可能性があります。GraphQLはその特性上、前述のようなパフォーマンスと設計のトレードオフが発生しませんが、RSCも同様にこの問題を解消するため、これをメリットとして享受できません。それどころか、RSCとGraphQLを協調させるための知見やライブラリが一般に不足してるため、実装コストが高くバンドルサイズも増加するなど、デメリットが多々含まれます。

Hayato YokoyamaHayato Yokoyama

https://zenn.dev/akfm/books/nextjs-basic-principle/viewer/part_1_colocation#設計・プラクティス

App RouterではServer Componentsでのデータフェッチが利用可能なので、できるだけ末端のコンポーネントへデータフェッチをコロケーションすることを推奨[2]しています。

GraphQLはルート階層で過不足なく1回でフェッチする思想(だと思う)から、RSCの末端でフェッチする思想とは相反する