概要
この記事では、Next.jsを使った開発において、つい見落としがちだけど、意識したいポイントについて簡単にまとめました。
目次
設計観点
- SSR / ISR / SSG を選択する
- コンポーネントツリーの末端をClient Componentにする
- コンポーネントの設計に迷ったらサードパーティのコンポーネントの実装を見る
- layout.tsx で 認可をしない
データフェッチ観点
- Server Componentsでデータをfetchする
- データフェッチ層はserver-onlyで保護する
- Request Memoizationの活用
- バージョン変更を考慮してfetchの第二引数は必ず指定する
- fetchが利用できない場合はReact.cacheでキャッシュ戦略を最適化する
コンポーネント実装観点
- 画像にnext/imageを使用する
- next/routerではなくnext/navigationからインポートする
- メソッド戻り値の指定が不足していないか確認する
- 重いコンポーネントのレンダリングは Streaming SSR を活用する
- キャッシュ済みのページやデータを更新するにはrevalidatePathとrevalidateTagを使用する
テスト観点
本文
SSR / ISR / SSG を選択する
Next.jsでは、ページの生成方法として主に3つの戦略があります:サーバーサイドレンダリング(SSR)、インクリメンタル静的再生成(ISR)、静的サイト生成(SSG)です。
ページの要件に応じて、最適な方法を選択しましょう。
方式 | パフォーマンス | 更新頻度 | 推奨シーン |
---|---|---|---|
SSG | 高 | 低 | 静的コンテンツ |
ISR | 中 | 中 | 定期的に更新されるコンテンツ |
SSR | 低 | 高 | 動的で常に変化するコンテンツ |
指定方法
page.tsx
で指定します。
// SSR
export const dynamic = 'force-dynamic'
// ISR
export const revalidate = 10;
// SSG
export const dynamic = 'force-static'
コンポーネントツリーの末端をClient Componentにする
コンポーネントツリーの末端を Client Components
にすることで、Client Boundary
を下層に限定することができます。
例えば検索バーを持つヘッダーを実装する際に、ヘッダーごと Client Components
にするのではなく検索バーの部分だけClient Components
として切り出し、ヘッダー自体は Server Components
に保つことを検討しましょう。
Compositionパターンを活用する
上記の方法はシンプルな解決策ですが、どうしても上位層のコンポーネントを Client Components
にする必要がある場合もあります。その際には Composition
パターンを活用して、Client Components
を分離することが有効です。
Client Components
は Server Components
をimportすることができませんが、コンポーネントツリーとしてはClient ComponentsのchildrenなどのpropsにServer Componentsを渡すことで、レンダリングが可能です。
🔍 参考:
コンポーネントの設計に迷ったらサードパーティのコンポーネントの実装を見る
コンポーネントの設計に迷った場合は、よく使われているサードパーティのコンポーネントライブラリの実装を参考にすることが効果的です。
- Radix UI: アクセシビリティに配慮した低レベルコンポーネント
- Chakra UI: 柔軟なカスタマイズが可能なコンポーネント
- shadcn/ui: スタイルとロジックが分離されたヘッドレスUIコンポーネント
layout.tsx で 認可をしない
特定パス配下の認可を実装する場合、layout
ではなく middleware
で行いましょう。
App Router
においてページとレイアウトは並行にレンダリングされるため、必ずしもレイアウト層に実装した認可チェックがページより先に実行されるとは限りません。
基本的に認可処理はmiddleware
で行うようにしましょう。
Server Componentsでデータをfetchする
基本的にデータの取得は 「Server Components」 で行うようにしましょう。
Server Components でリクエストを行うことのメリット
- Next.js サーバーと API サーバーは高速で安定しているため、リクエストが高速処理される
- API のエンドポイントをパブリックなネットワークに公開せずに済む
データフェッチ層はserver-onlyで保護する
Next.js 14以降では、server-only
パッケージを使用して、データフェッチ層のモジュールを Client Components
に誤ってインポートした場合にビルド時エラーを発生させることができます。
データフェッチ層を誤ってクライアントサイドで利用することを防ぐためにも、server-only
パッケージを利用しましょう。
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
Request Memoizationの活用
App Router
ではデータフェッチをコロケーションすることが推奨されています。
しかし末端のコンポーネントでデータフェッチを行うと、ページ全体を通して重複するリクエストが発生する可能性が高まります。
App Router
はこれに対処するため、レンダリング中の同一リクエストをメモ化し排除するRequest Memoization
を実装しています。
設計・プラクティス
Request Memoization
がリクエストを重複と判定するには、同一URL・同一オプションの指定が必要で、オプションが1つでも異なれば別リクエストが発生します。
オプションの指定ミスにより Request Memoization
が効かないことを防ぐため、複数のコンポーネントで利用するデータフェッチ処理は分離しましょう。
export async function getProduct(id: string) {
const res = await fetch(`https://dummyjson.com/products/${id}`);
return res.json();
}
バージョン変更を考慮してfetchの第二引数は必ず指定する
Next.jsの以前のバージョンでは、fetchを使用するとデフォルトのキャッシュ値がforce-cache
になっていました。これはバージョン15で変更され、デフォルトはcache: no-store
になりました。
つまり、Next.js 15からはデフォルトでキャッシュは使用されなくなりました。明示的にキャッシュを利用したい場合はcache: 'force-cache'
を指定する必要があります。
今後、バージョンアップデートによる動作変更の影響を受けないようにするため、基本的に引数は明示的に指定しておくようにしましょう。
await fetch('https://api.vercel.app/blog', { cache: 'no-store' })
fetchが利用できない場合はReact.cacheでキャッシュ戦略を最適化する
GraphQL
を使用しているなど、拡張された fetch
を使用できない場合や、より細かいキャッシュ制御が必要な場合、React.cache
を利用しましょう。
React.cache
React.cache
は、関数の結果をメモ化するためのユーティリティです。
同じ引数で呼び出された場合、計算を再実行せずにキャッシュされた値を返します。
GraphQL(ApolloClient) での実装例
GraphQLを使用する場合、Apollo Clientのclient.query
をReact.cacheでラップすることで、リクエストをキャッシュ化できます。
import { apolloClient, GetPostDocument } from "@/app/graphql";
import { cache } from "react";
export const getPost = cache(async (id: string) => {
const { data } = await apolloClient.query({
query: GetPostDocument,
variables: { id }
});
return { json: () => Promise.resolve(data.post) };
});
画像にnext/imageを使用する
Next.jsのプロジェクトでは、img
タグではなく、next/image
を使用することが推奨されています。
Imageタグの機能
- 画面サイズに応じて画像を最適化(
webp
,avif
などに自動変換) - 画像の表示領域を画像がダウンロードされる前から確保し
Cumulative Layout Shift (CLS)
を防止 - 遅延読み込み
import Image from "next/image";
<Image
src={dogImage}
alt="dogImage"
width={500}
height={500}
/>
next/routerではなくnext/navigationからインポートする
Next.js 13以降のApp Router環境では、next/navigation
モジュールからルーティング関連の機能をインポートする必要があります。
// ❌
import { useRouter } from 'next/router';
// ⭕️
import { useRouter } from "next/navigation";
これは、App Router向けの新しいナビゲーションAPIが提供されているためです。
間違ったモジュールからインポートすると、機能しないだけでなく、実行時エラーの原因になります。
メソッド戻り値の指定が不足していないか確認する
TypeScript
に関連する話題ですが、メソッド戻り値の型指定が必要であるか必ず確認しましょう。
型推論では目的の型と認識されない場合は、明示的に型指定することを忘れないようにしましょう。
// ❌ 型が不明確
async function getTodoList() {
const response = await fetch(`${apiEndpoint}/todos`);
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
}
// => (local function) getTodoList(): Promise<any>
// ✅ 型が明確
async function getTodoList(): Promise<Todo[]> {
const response = await fetch(`${apiEndpoint}/todos`);
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
}
// => (local function) getTodoList(): Promise<Todo[]>
重いコンポーネントのレンダリングは Streaming SSR を活用する
Dynamic Renderingを使用する場合でも、特に重いコンポーネントのレンダリングは<Suspense>
で遅延させ、Streaming SSRを活用しましょう。
Suspenseによる非同期処理の分離
Suspenseは、データ取得やコンポーネントの読み込みなどの非同期処理を、レンダリングプロセスから分離します。これにより、重いコンポーネントの処理を待たずに、ページの他の部分を先に表示することができます。
import { Suspense } from 'react';
import MyAsyncComponent from './MyAsyncComponent';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyAsyncComponent />
</Suspense>
);
}
Streaming SSRのメリット
Server Components
と組み合わせることで、HTMLを段階的にストリーミングできます。全てのデータが揃うまで白い画面を表示するよりも、できた部分から表示してユーザーにコンテンツを見せることができます。
App Router
では Streaming SSR
をネイティブにサポートしており、重いデータフェッチを伴う Server Components
のレンダリングを遅延させ、ユーザーにいち早くレスポンスを返し始めることができます。
デメリットと考慮点
画面の一部にfallbackを表示しそれが後に置き換えられるため、いわゆるLayout Shiftが発生する可能性があります。特にユーザーインターフェースの重要な部分では、適切なサイズのスケルトンUIやプレースホルダーを用意することでこの問題を軽減できます。
キャッシュ済みのページやデータを更新するにはrevalidatePathとrevalidateTagを使用する
SSG
や ISR
で生成されたページやデータをオンデマンドで更新したい場合は、revalidatePath
やrevalidateTag
関数を使用します。
-
revalidatePath
: 特定のパスに関連するキャッシュを無効化 -
revalidateTag
: 特定のキャッシュタグに関連するデータを無効化
これらを使うと、新しいコンテンツ作成後や更新後に、関連するキャッシュを即時に更新できます。
export async function createPost(id: string) {
// Postの作成
revalidatePath('/posts')
redirect(`/post/${id}`)
}
DOM取得は getByRole を優先する
React Testing Library
を使用したテストでは、DOM要素の取得方法には優先順位があります。この優先順位はユーザーの実際の操作体験に近づけることを基準としています。
要素を取得するためのマッチャーとして、getByRole
を優先使用するようにしましょう!
getByRole
getByLabelText
getByPlaceholderText
getByText
getByDisplayValue
// ❌
const button = screen.getByTestId('submit-button');
// ✅
const button = screen.getByRole('button', { name: '送信' });
🔍 参考:
まとめ
以上です!
今後も定期的に更新していこうと思います🙋♂️
(コメントも大歓迎です)
良いエンジニアライフを!