【React】複数のデータフェッチは並列で取得しよう
はじめに
最近、Next.jsのApp RouterでもRemixでも開発することが多々あります。
そこで、要件によっては1ページで複数のデータフェッチを行うことがあり、その際に興味本位ですが、直列と並列でパフォーマンスを比較してみました。
その結果とともに、RSC環境だったり、Reactのフレームワークでは複数のデータフェッチは並列でやることをおすすめしたいと思い、本記事を記述しようと思いました。
TL;DR
- データ取得が完了するまでコンポーネントはレンダリングされない
- ユーザーのリクエストに対してレンダリングはなるべく早く完了させる
- 複数のデータフェッチは並列にしよう
直列・並列のデータフェッチとは
まず、初心者の方向けに直列と並列のデータフェッチに違いを解説します。
※ 知っているよという方は本項は飛ばしてください
直列の処理
まずは直列の処理ですが、以下のような記述のことを指します。
type Props = Readonly<{ params: { id: string }, searchParams: { [key: string ]?: string | string[] } >
const SeriesComponent: FC<Props> = async ({ params, searchParams } ) => {
const categoryName = typeof searchParams === 'string' ? searchParams.categoryName : ''
const posts = await fetch(`http://localhost:3000/posts/${params.id}`)
const categories = await fetch(`http://localhost:3000/category?categoryName=${categoryName}`
return (
// ・・・省略
)
}
並列の処理
次に並列の処理ですが、以下のような記述のことを指します。
type Props = Readonly<{ params: { id: string }, searchParams: { [key: string ]?: string | string[] } >
const SeriesComponent: FC<Props> = async ({ params, searchParams } ) => {
const categoryName = typeof searchParams === 'string' ? searchParams.categoryName : ''
const [posts, categories] = await Promise.all([
fetch(`http://localhost:3000/posts/${params.id}`)
fetch(`http://localhost:3000/category?categoryName=${categoryName}`
])
return (
// ・・・省略
)
}
なぜ複数のデータフェッチは並列ですべきなのか
複数のデータフェッチの場合、直列に処理を行うと、1つのデータフェッチが完了するまで次のフェッチを開始できないため、総合的な処理時間が長くなってしまいます。
これはレンダリングの完了を遅らせる原因になります。
また、1つ1つの処理の中にとんでもなく重たい処理があった場合、それも完了するまで待って、次のフェッチをするということもあるため、レンダリングがその分遅れてしまいます。
そこでPromise.all
を用いて並列処理とすることで上記の問題を改善しようという流れになります。
並列にデータフェッチを行えば、すべてのフェッチを同時に開始できるため、最も遅いフェッチを待つだけで済みます。つまり、全体の処理時間が最も速いフェッチの時間に収まる可能性があり、ユーザーに早くコンテンツを表示できます。
また、JavaScriptはシングルスレッドのため、1つの非同期処理が完了するまで次の処理を実行できません。並列処理を行えば、この制約を回避し、CPUを効率的に活用できます。
つまり、ユーザーのリクエストに素早く応答するために並列に処理を走らせて、レンダリングをなるべく素早く完了させようよということです。
ただし、何でもかんでも並列処理にするというのは良くないです。
どんなことにもメリット・デメリットが有るように並列処理にもデメリットがあります。
代表的なものでいうと以下の2つが挙げられます。
- ネットワークリソースを多く消費すること
- 処理の順序が保証されない
1つ目については帯域幅が制限されている環境で問題になる可能性があります。
なので、これはアプリケーションの要件による部分が強いので、多くの場合、そこまで意識する必要はないでしょう。
問題は2つ目です。
すべてを並列処理にすると例えば、同じ並列処理をしている処理の中にある処理のレスポンスデータを必要とする処理がある場合、「このときは成功したけど、今度は失敗した」ということが起こりかねません。
例えば、userIdでデータを取得する処理とuserを取得する処理を同じ並列処理の中にいれると順序が保証されないので、問題となります。
この場合は、直列で確実にuserを取得してから処理を行う必要があります。
そのため、ユースケースに応じて並列処理と直列処理を使い分ける必要があるのです。ここでは、特にUXへの影響が大きい場合は、並列処理によるメリットが大きくなるということです。
この点に留意して使い分けられるようにしておきましょう。
Remixでの結果
今回は、サンプルとは違い、Next.jsよりパフォーマンスが高いと言われるRemixで検証してみました。
今回は以下のように直列で行っていた処理を並列にして検証してみることにしました。
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await authenticator.isAuthenticated(request, {
failureRedirect: '/login',
})
const projects = await getAllProjects(true)
const missions = await getAllMissions(true)
const categoryOfTroubles = await getAllCategoryOfTroubles()
const categoryOfAppeals = await getAllCategoryOfAppeals()
const dailyReports = await getDailyReportByUserId({ userId: user.id })
return json({
projects,
missions,
categoryOfTroubles,
categoryOfAppeals,
dailyReports,
})
}
こちらが、並列にしたときのコードです。
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await authenticator.isAuthenticated(request, {
failureRedirect: '/login',
})
const [
projects,
missions,
categoryOfTroubles,
categoryOfAppeals,
dailyReports,
] = await Promise.all([
getAllProjects(true),
getAllMissions(true),
getAllCategoryOfTroubles(),
getAllCategoryOfAppeals(),
getDailyReportByUserId({ userId: user.id }),
])
return json({
projects,
missions,
categoryOfTroubles,
categoryOfAppeals,
dailyReports,
})
}
検証結果
直列での処理は上の2つで、並列での処理は4つ目、5つ目が結果となります。
微々たる差かもしれませんが、これはloader関数のみのレスポンスの時間なので、ページ全体の結果ではないです。
もし、重たいコンポーネントやサーバーの処理があった場合、もっと結果は顕著にでると考えられます。
ですので、先程も言ったようにユースケースに応じた対応ができる必要があります。
まとめ
ということで、できる限り並列にできる箇所は並列にして処理を行っていきましょう。
それが元々速いNext.jsやRemixのレスポンス速度のさらなる向上につながり、開発体験だけでなくUXもいいアプリケーションの構築につながるはずです。
Discussion