本記事は TypeScript + Next.js 学習リポジトリ の実装をベースに、Next.js App Router におけるレンダリング( Server Component / Client Component , Loading UI / Streaming )について解説します。
前回の記事:【Next.js App Router】5つのルーティングについて もあわせてどうぞ。
はじめに
Next.js の App Router では、Reactと比べてレンダリング方法が大きく変わりました。本記事では以下の4つを順に解説します。
1. サーバーコンポーネント
基本的な考え方
一言でいうと JavaScript をサーバー側で実行し、完成した HTML をクライアントに返す という概念。
App Router の page.tsx は デフォルトでサーバーコンポーネント として扱われます。'use client' を付けない限りサーバー側で実行され、HTMLとして組み立てられたものがブラウザに届きます。
React との違い
従来の React(CSR)と Next.js のサーバーコンポーネントの違いを整理すると以下のようになります。
| React(CSR) | Next.js(サーバーコンポーネント) | |
|---|---|---|
| サーバー | HTML, JavaScriptが置いてある | JS実行、HTML生成 |
| ブラウザ | 受け取ってJS実行、HTML生成 | HTMLを受け取る |
React の場合、ブラウザがJavaScriptをダウンロードして初めてHTMLを組み立てるため、初期表示までに遅延があります。
一方Next.jsのサーバーコンポーネントでは、サーバー側であらかじめHTMLを生成してから返すので、ブラウザは届いたHTMLを描画するだけでよくなります。
メリット
- 処理が速い:パワーのあるサーバー側でJavaScriptを事前に実行するため、ブラウザの負荷が下がる
- SEOに強い:コンテンツが完成した状態のHTMLとして配信されるため、クローラーが内容を把握しやすい
デメリット
- イベントリスナーや
windowメソッドなど、ブラウザ依存のAPIが使えない
💡 このデメリットを解決するのが、次の クライアントコンポーネント です。
サーバーコンポーネントを使ったデータ取得の例
サーバーコンポーネントの真骨頂は、コンポーネント関数を async にしてデータ取得を直接書ける ことです。
// app/page.tsx
type Post = {
id: number;
title: string;
};
// サーバーコンポーネントではページ初期化関数を async にできる
export default async function Home() {
// Next.js での fetch() は取得したデータをサーバーのキャッシュに保存するため、処理が速い
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: Post[] = await res.json();
return (
<div>
<h1>記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
React で同じことをやるなら…
React(クライアント側)で書こうとすると、useState と useEffect を組み合わせる必要があります。
// React でのデータ取得(参考)
const [posts, setPosts] = useState();
useEffect(() => {
const fetchPosts = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
setPosts(data);
};
fetchPosts();
}, []);
サーバーコンポーネントでは useState も useEffect も不要です。async / await で直接データを取得し、そのままJSXに渡せます。これが 「初期データの描画が速い」 最大の理由です。
💡 Next.js の
fetch()は標準のfetchを拡張したもので、取得結果をサーバー側でキャッシュしてくれるため、同じURLへのリクエストは2回目以降高速になります。
2. クライアントコンポーネント
基本的な考え方
サーバーコンポーネントだけではボタンクリックなどのインタラクションを扱えません。そこで Next.js は コンポーネントごとにサーバー実行・クライアント実行を切り替えられる仕組み を用意しています。
考え方の整理:
- コンテンツの初期表示に関わるロジック → サーバーで実行
- インタラクティブな動作のロジック → ブラウザで実行
- 両者を紐付けることを ハイドレーション(Hydration) と呼ぶ
| コンポーネントの初期化ロジック | インタラクティブな動作用ロジック | |
|---|---|---|
| サーバー | JS実行,HTML生成 | JSのまま保持 |
| ブラウザ | ハイドレーションされた(HTML+JS)を受け取る | |
つまり、サーバー側でHTMLを生成しつつ、インタラクティブな部分のJSは後からブラウザに送って「合体」させる、というイメージです。
使用方法
ファイルの最上部に 'use client' と書くだけ。これでそのコンポーネントはクライアントコンポーネントになります。
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
クリック数: {count}
</button>
);
}
使い分けのポイント
| ケース | どちら? |
|---|---|
| データ取得して表示するだけ | サーバーコンポーネント |
useState、useEffect を使う |
クライアントコンポーネント |
onClick などのイベントハンドラ |
クライアントコンポーネント |
window、document を使う |
クライアントコンポーネント |
| SEO重要・初期表示を速くしたい | サーバーコンポーネント |
💡 大原則:できるだけサーバーコンポーネントで作り、必要な部分だけ
'use client'で切り出す。これでバンドルサイズを最小化できます。
3. Loading UI
概要
Next.js では loading.tsx という 予約ファイル を作成するだけで、同階層の page.tsx を読み込んでいる間に表示される ローディング画面 を簡単に実装できます。
実装例
// app/about/loading.tsx
export default function Loading() {
return <h1>Loading...</h1>;
}
たったこれだけ。/about にアクセスして page.tsx の処理(データ取得など)が完了するまで、この Loading コンポーネントが自動的に表示されます。
仕組み
ユーザーが /about にアクセス
↓
app/about/loading.tsx が即座に表示される
↓
app/about/page.tsx の処理(重い処理・データ取得)が完了
↓
loading.tsx → page.tsx に切り替わる
ポイント
- ファイル名は 必ず
loading.tsx(予約ファイル) - 同階層の
page.tsxの読み込み中だけでなく、その配下のページにも適用される - 内部的には次に説明する Suspense が自動で使われている
4. ストリーミング
概要
ストリーミングとは、読み込みが完了したデータから随時描画していく 概念。
ページ全体の準備が完了するのを待たず、準備できたパーツから先に表示 することで体感速度を大きく向上させられます。Next.js では <Suspense> タグを使うことでこれを実装できます。
イメージ
通常のレンダリング:
[全部の処理が終わるまで真っ白] → 一気に全部表示
ストリーミング:
[すぐ表示できる部分を先に表示] → [遅い部分はあとから順次表示]
実装例
ここでは「3秒かかる重いコンポーネント」を別ファイルに分け、メインコンテンツより遅れて表示される様子を確認します。
重いコンポーネント
// app/about/SlowComponent.tsx
export default async function SlowComponent() {
// わざと3秒待たせる
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});
return <div>重いコンポーネント</div>;
}
Suspense で囲んでストリーミング
// app/about/page.tsx
import { Suspense } from 'react';
import SlowComponent from './SlowComponent';
export default function About() {
return (
<>
<h1>メインコンテンツ(すぐに表示)</h1>
{/* Suspense で囲まれた中身が読み込まれるまで、fallback の中身が描画される */}
<Suspense fallback={<h1>コンポーネントを読み込み中...</h1>}>
<SlowComponent /> {/* ← 読み込みに3秒かかるよう設定している */}
</Suspense>
</>
);
}
動作フロー
/about にアクセス
↓
即座に表示される:
┌─────────────────────────────┐
│ メインコンテンツ(すぐに表示)│
│ コンポーネントを読み込み中... │ ← fallback
└─────────────────────────────┘
↓ (3秒経過)
SlowComponent の準備が完了
↓
fallback が SlowComponent に置き換わる:
┌─────────────────────────────┐
│ メインコンテンツ(すぐに表示)│
│ 重いコンポーネント │ ← 完成版
└─────────────────────────────┘
Loading UI と Suspense の使い分け
| 用途 | 使うもの |
|---|---|
| ページ全体に対するローディング表示 | loading.tsx |
| ページ内の 特定のコンポーネント だけ | <Suspense> |
loading.tsx は ページ単位、Suspense は コンポーネント単位 での使い分けが基本です。
まとめ
| トピック | 一言で | キーワード |
|---|---|---|
| サーバーコンポーネント | サーバーでJS実行 → HTML を返す | デフォルト、async
|
| クライアントコンポーネント | ブラウザで動作。インタラクティブ用 | 'use client' |
| Loading UI | ページ読み込み中の画面 | loading.tsx |
| ストリーミング | 準備できた部分から順に描画 | <Suspense> |
Next.js App Router の世界では、「すべてサーバーコンポーネントで作り、必要な部分だけクライアントコンポーネントに切り出す」 が基本戦略です。
そこに loading.tsx と <Suspense> を組み合わせることで、初期表示が速く、UX が良いアプリケーションを作ることができます。