Next.js 13.4の変更点について
お久しぶりです。 転職してから2ヶ月が経過しました。
つまり、PdMとしても2ヶ月が経過したことになりますが、 皆様はいかがお過ごしでしょうか?
PdMになってからは、ほぼコードを書かなくなりましたが、エンジニアとしてのスキルを維持するために、Next.js 13に触れてみることにしました。
他にもnext/linkやnext/fontなどの機能もありますが、今回はメインのApp DirectoryとServer Componentsに焦点を当ててお話ししたいと思いますので、それ以外の部分は省略させていただきます。
1.Next.js 13
少し前からNext.jsの13のマイナーバージョンリリースは行われていましたが、先月、Next.js 13の目玉機能であるApp RouterのStable版であるNext 13.4がリリースされました。
また、今後正式にStable版として提供されるであろうServer Actionsなどの他の機能もリリースされています。
2.App Directory
これまでのNext.jsでは、Page Routerがベースとなっていました。
Page Routerでは、/pagesディレクトリ以下にページを作成することで、/page以下のディレクトリベースでルーティングが行われていました。
しかし、今回のApp Routerでは、新たに/appディレクトリが導入されました。
App Routerでは、/appディレクトリベースでのルーティングが行われるようになります。
早速、ドキュメントの通りに進めて、Next.jsの導入からはじめましょう。
$ npx create-next-app@latest
No / Yesの選択肢を使用して設定を進めていきます。
What is your project named? my-app
Would you like to use TypeScript with this project? No / Yes
Would you like to use ESLint with this project? No / Yes
Would you like to use Tailwind CSS with this project? No / Yes
Would you like to use `src/` directory with this project? No / Yes
Use App Router (recommended)? No / Yes
Would you like to customize the default import alias? No / Yes
無事にインストールが完了すると、以下のようなディレクトリ構造になります。
app
|── api
| |── hello
| └── route.ts // /api/hello
|── favicon.ico
|── globals.css
|── layout.tsx // _document.tsxの代替で大元のベースレイアウト
|── page.tsx // /indexと同じ
|── // 他設定系のファイルが続く
ディレクトリルートである/app直下のlayout.tsxは、少し特殊な役割を持っています。
// New: App Router ✨
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}
appディレクトリ直下にあるlayout.tsxは特殊で、RootLayoutと呼ばれます。このファイルには<html>や<body>タグを含める必要があり、これは従来の_document.tsxの代替となります。
2-1.新しいページを作るには?
/app/post/page.tsx
ディレクトリの中にpage.tsxというファイルを作成すると、/postでアクセスできるようになります。
App Routerでもディレクトリベースの概念は変更されていません。
/app/posts/[id]/page.tsx
とすれば、パラメータごとに/posts/:idページを表示することも可能です。
3.Layout
/app直下にはlayout.tsxというファイルが存在しますが、ページごとに異なるlayoutファイルを適用することも可能です。
先ほど作成した/app/postディレクトリ内にlayout.tsxを作成します。
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>{children}</body>
</html>
)
}
// app/post/layout.tsx
export default function Layout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
{children}
</section>
)
}
つまり、/postにアクセスしたときのlayout構造は以下のようになります。Nested Layoutとして展開されます。
// - 外側: /app/layout.tsx
// - 内側: /post/layout.tsx
<!-- app/layout.tsxは一番外側に適用 -->
<html lang="ja">
<body>
<!-- app/post/layout.tsxはapp/layout.tsxにラップされて入れ子で適用 -->
<section>
<!-- app/post/page.tsxがここに表示されます -->
</section>
</body>
</html>
これまでは、レイアウト用のコンポーネントを作成し、各ページに適用する必要がありました。
しかし、適用させたいルーティングに沿ってlayout.tsxを作成することで、自動的にNested Layoutとして機能させることができます。
4.metadata
これまでは、_document.tsxでメタ情報を条件に応じて表示していましたが、layout.tsxやpage.tsxで設定することができるようになりました。
もしlayout.tsxとpage.tsxの両方にメタデータが含まれている場合、page.tsxのメタデータが優先されます。
// app/page.tsx
export const metadata = {
title: 'Top',
description: 'Top Page',
}
// app/post/page.tsx
export const metadata = {
title: 'Post',
description: 'Post Page',
}
5.Server Components
React Server Componentsは、Reactコンポーネントをサーバーサイドでレンダリングする技術です。
Next.jsの13以降では、デフォルトでこのコンポーネントが使用されます。
大半のコンポーネントは非インタラクティブであることがほとんどであり、非インタラクティブなコンポーネントについてはServer Componentsの使用が推奨されています。
Server ComponentsからClient Componentsを呼び出したり、Propsを渡すことも可能です。
サーバーコンポーネントとクライアントコンポーネントの使用タイミングについては、ドキュメントを参照してください。
5-1.バンドルサイズの減少
Server Componentsでは、クライアントがダウンロードする必要のあるライブラリなどはバンドルに含まれません。
クライアントは単にレンダー結果のJSXを受け取るだけです。
再レンダリングが必要な場合は、再度サーバーにレンダーリクエストを送る必要があります。
そのため、ユーザーのインタラクションに反応しない多くのコンポーネントをクライアントのバンドルから除外することができるため、パフォーマンスが向上します。
5-2.バックエンドのリソースにアクセスが可能
Server Componentsでは、サーバーサイドで処理されるため、バックエンドのリソース(データベースや環境変数など)に直接アクセスすることができます。
5-3.Server Componentsの制約
サーバーサイドでのみ動作するため、Server ComponentsではフックやブラウザーのAPIの使用は制限されています。
さらに、リレンダーに関連するuseEffectやuseContextなども使用できません。
また、もし3rdパーティ製のコンポーネントがクライアント側で実行が必要なJavaScriptを含んでいる場合、それらを正しく動作させるためにはClient Componentでラップする必要があります。
5-4.データフェッチとキャッシュ
これまではgetServerSideProps / getStaticProps / getServerSideProps / getInitialPropsといった方法でデータを取得していましたが、/appディレクトリ以下ではこれらの方法はサポートされていません。
代わりにServer Componentsでは、fetch optionsオブジェクトを拡張したfetchを使用してデータを取得します。
Next.jsチームはServer Components間でpropsを使ったデータの受け渡しではなく、Server Component内で直接fetchをすることを推奨しています。
そのため、Server Componentsでfetch APIを使用すると、Next.jsがリクエストの重複を排除し、効率的にデータをキャッシュして取得してくれます。
同じデータを複数のコンポーネントで取得する場合でも、1回のリクエストで済むようになります。
さらに、各コンポーネントの静的・動的レンダリングの設定や、ISRにおける再検証のタイミングなども設定することができます。
詳細はドキュメントを参照してください。
5-5.asyncとSuspense
Server Componentでは非同期処理が可能です。
そのため、asyncを使用し、Promiseを<Suspense>で処理することができます。
もしSampleComponentがServer Componentである場合、<Suspense>でラップすることで非同期に読み込むことができます。
export default function Home() {
return (
<Suspense fallback={<div>Loading...</div>}>
<h1>Page</h1>
{/* @ts-expect-error Server Component */}
<SampleComponent /> // 非同期で読み込みServer Component
</Suspense>
)
}
6.Client Components
これまでのコンポーネントと同様のものはClient Componentsと呼ばれます。これを使用するのは、インタラクションが必要な場合やフックを使用してアプリケーションの状態を管理する場合です。
デフォルトではServer Componentsとして展開されますが、Client Componentsとして展開するには、ファイルのトップレベルに'use client'と記述する必要があります。
Client ComponentからServer Componentをインポートする場合、直接コンポーネント内にネストするのではなく、親のServer ComponentからChildrenとして渡す必要があります。
以下のように、Client ComponentからServer Componentsを直接インポートすることはできません。
ExampleClientComponent は、Client Components であり、children を受け入れるスロットを設けます。
親の Server Components 内で、ExampleClientComponent をインポートします。
ExampleClientComponent の children 内で、Server Component をインポートします。
7.SSR
Serverという名前がついているため、勘違いしやすいですが、Server Components は SSR(サーバーサイドレンダリング)とは異なります。
従来のSSRでは、ページ単位で getServerSideProps / getStaticProps / getServerSideProps / getInitialProps を使用して、HTMLをどのタイミングでどのように返すかという概念でした。
しかし、Next.js 13からはこの概念が変わりました。
7-1.静的レンダリングと動的レンダリング
とレンダリングについてのドキュメントを見ると、コンポーネント単位での静的レンダリングや動的レンダリングが可能であることが明記されています。
静的レンダリングでは、あらかじめ生成されたHTMLが返される一方、動的レンダリングではクライアント側でコンポーネントが生成され、その結果のHTMLが返されることがわかります。
しかし、具体的にどのようにHTMLが出力されるのかという点については、さらにドキュメントを調査する必要があります。
7-2.Static Rendering (Default)
デフォルトでは、Next.jsはパフォーマンスを向上させるためにルートを静的にレンダリングします。これは、すべてのレンダリング作業が前もって行われ、ユーザーに地理的に近いコンテンツデリバリーネットワーク(CDN)から提供されることを意味します。
7-3.Static Data Fetching (Default)
デフォルトでは、Next.jsは、キャッシュ動作を特にオプトアウトしていないfetch()リクエストの結果をキャッシュするようになります。つまり、キャッシュオプションを設定しないfetchリクエストは、強制キャッシュオプションを使用します。
ルート内のフェッチリクエストでrevalidateオプションが使用されている場合、ルートは再バリデーション中に静的に再レンダリングされます。
7-4.Dynamic Rendering
静的レンダリング中に、動的関数または動的fetch()リクエスト(キャッシュなし)が検出されると、Next.jsはリクエスト時にルート全体を動的にレンダリングするように切り替わります。キャッシュされたデータリクエストは、動的レンダリング中も再利用することができます。
ここで知りたいのはgetServerSidePropsと同じ動きです。
つまり、7-4に記載されているように、動的な関数や動的なfetch()リクエストによって、コンポーネントが動的にレンダリングされることがわかります。
この動的なレンダリングにより、getServerSidePropsにおいてキャッシュを使用しない場合と同様の動作が実現されているようです。
しかし、具体的にはどのように実現されているのか、そのためのデータフェッチについても調査する必要があります。
7-5.Blocking Rendering in a Route
7-6.結局どういうこと?
結論を述べると、コンポーネントレベルでデータをフェッチ、キャッシュ、再バリデートする柔軟な方法が1つ増えたと言えます。
つまり、これまでトップレベルのページ単位でしか使えなかったgetServerSidePropやgetStaticPropsが、動的関数または動的fetch()リクエストにより個々のServer Componentsで実行できるようになりました。
以下のfetchの使い分けで、柔軟に行えるようになったことになります。
// getStaticPropsしたものに近い
// default: force-cacheで省略可能
fetch(URL, { cache: 'force-cache' });
// getStaticPropsでrevalidateしたものに近い
fetch(URL, { next: { revalidate: 10 } });
// getServerSidePropsしたものに近い
fetch(URL, { cache: 'no-store' });
詳しくはこちらのドキュメントを参照ください。
Server Componentsがこのような思想になっている理由の一つとして、以下のような情報を、ひとつのページ内で取得するブログを例に挙げて説明します。
さきほど、各々のServer Componentsが動的にデータを取得できることをお伝えしました。
さらに、Suspenseが導入されたことにより、Server Componentsは非同期的にデータをストリーミングしながら取得できるようになりました。
つまり、おすすめの特集記事一覧を取得する際にレスポンスが遅くなっても、最初に個別記事の情報を返すことができ、その後でストリーミングを介しておすすめの特集記事一覧の部分を追加していくことが可能です。
これにより、レスポンス待ちの時間を短縮し、ユーザーエクスペリエンスを向上させることができるようになりました。
Server Componentsでは、Suspenseと動的fetch()リクエストを組み合わせることで、コンポーネントごとにどのタイミングでどのようにレンダリングするかを容易に管理することができます。
8.SSG
これまでのSSGでは、getStaticPathsを使用して記事の一覧を取得し、ビルドプロセスで静的なファイルとして作成していました。
しかし、Next.js 13からはgenerateStaticParamsという機能を使用します。
generateStaticParamsを利用することで、Dynamic Routesの情報をビルド時に取得し、静的なファイルとして生成することができます。
これにより、より効率的な静的サイトの生成が可能となります。
// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default function Page({ params }) {
const { slug } = params
// ...
}
例のように、generateStaticParams()の中では記事の一覧をfetch()リクエストで取得しています。
記事の更新を再検証したい場合は、fetch(URL, { next: { revalidate: 10 } }); のようにrevalidateを使用することができます。
これにより、SSGとしての動作を実現できます。
また、完全に静的なエクスポートを行いたい場合は、next.config.jsにパラメータを追加することでStatic Exportを利用できます。
今回の変更は多くの破壊的な変更が含まれており、プロジェクトに反映させるためには大きな工数がかかるでしょう。
しかし、エコシステムや新機能を早く活用したい場合は、Next.js 13への移行が必要です。
また、13に移行することでページのレンダリングパフォーマンスの改善も期待できるため、ビジネス的にも導入する価値があるでしょう。
それでは。