「Server Componentsがすごいらしいけど、結局どう使えばいいの...?」
Next.js App Router、公式ドキュメント読んでも「で、うちのプロジェクトではどうすれば」がわからない。その気持ち、わかります。
playparkでは複数の業務システムをNext.js + TypeScriptで構築してきました。本記事では、マーケティング記事ではなく、実際のプロジェクトから得られた本音の知見を共有します。(キラキラした成功談だけじゃなく、「最初からそうしろよ」案件も含めて)
実績サマリー:この記事の根拠データ
| 改善項目 | Before | After | 改善率 | 測定方法 |
|---|---|---|---|---|
| API処理時間 | 20〜30秒 | 3〜5秒 | 85%削減 | console.time() + Cloud Monitoring |
| データ結合処理 | 10秒 | 1秒未満 | 90%削減 | ローカルベンチマーク |
| JSバンドルサイズ | ベースライン | 40%削減 | 40%削減 | next build 出力 |
| LCP (Largest Contentful Paint) | 3.2秒 | 1.8秒 | 44%改善 | Lighthouse / PageSpeed Insights |
| TTI (Time to Interactive) | 4.5秒 | 2.1秒 | 53%改善 | Lighthouse |
測定環境: Next.js 14.x / Vercel + Cloud Run / Chrome DevTools + Lighthouse CI
この記事で学べること
- Next.js App Routerを業務システムに採用する判断基準
- Server ComponentsとAPI Routesの実践的な使い方
- API並列化とデータ構造最適化による具体的なパフォーマンス改善
- 本番運用で「やっておいてよかった」設計パターン
前提条件
- Next.js 14以降(App Router)の基本構文がわかる
- React Server Componentsの概念を知っている
- TypeScriptでの開発経験がある
「Server Componentsってなに?」という方は、まず公式ドキュメントを読んでから戻ってきてください。(この記事、その前提で書いてます)
実装方法
Step 1: Server Componentsでバンドルサイズを削減する
業務システムでは、ダッシュボードや管理画面などデータ表示がメインのページが多くなります。「表示するだけ」なのにJavaScriptをクライアントに送る必要、ありますか?
Server Componentsを活用することで、これらのページのJavaScriptバンドルを大幅に削減できます。
// app/dashboard/page.tsx
// 'use client' がないのでServer Component
import { RecentActivity } from '@/components/recent-activity';
import { StatsCard } from '@/components/stats-card';
interface DashboardData {
stats: {
totalUsers: number;
activeProjects: number;
completedTasks: number;
};
activities: Array<{
id: string;
action: string;
timestamp: string;
}>;
}
async function getDashboardData(): Promise<DashboardData> {
const res = await fetch('https://api.example.com/dashboard', {
next: { revalidate: 60 }, // 1分ごとに再検証
});
if (!res.ok) {
throw new Error('Failed to fetch dashboard data');
}
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<main className="container mx-auto py-8">
<h1 className="mb-6 text-2xl font-bold">ダッシュボード</h1>
<div className="grid gap-6 md:grid-cols-3">
<StatsCard stats={data.stats} />
</div>
<section className="mt-8">
<h2 className="mb-4 text-xl font-semibold">最近のアクティビティ</h2>
<RecentActivity activities={data.activities} />
</section>
</main>
);
}
ポイント: 'use client' を書かなければServer Component。データフェッチもレンダリングもサーバー側で完結します。クライアントに送るのはHTMLだけ。
効果: ダッシュボードページのJSバンドルが約40%削減。「体感で速くなった」とクライアントから言われました(数字より嬉しいやつ)。
Step 2: API Routes + Prismaでフルスタック開発
バックエンドを別途立てる必要がないプロジェクトでは、Next.jsのAPI Routesが有効です。「フロントもバックも俺だ」という小規模チームには特に刺さります。
// app/api/members/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
// リクエストのバリデーションスキーマ
const QuerySchema = z.object({
departmentId: z.string().optional(),
limit: z.coerce.number().min(1).max(100).default(50),
});
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const query = QuerySchema.parse({
departmentId: searchParams.get('departmentId') ?? undefined,
limit: searchParams.get('limit') ?? 50,
});
const members = await prisma.member.findMany({
where: query.departmentId
? { departmentId: query.departmentId }
: undefined,
include: { department: true },
take: query.limit,
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(members);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid query parameters', details: error.errors },
{ status: 400 }
);
}
console.error('Failed to fetch members:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
効果: フロントエンドとバックエンドの型が自動で共有され、「APIの戻り値の型が違う」という事故が激減。TypeScript、信じてよかった。
Step 3: API並列化で85%高速化
あるBI連携システムで、9個のAPIを順次呼び出していた処理がボトルネックになっていました。コーヒー2杯飲める待ち時間。(いや、仕事しろよ)
Before:直列処理
// ❌ 各APIを順番に待つ(律儀すぎる)
async function fetchAllData() {
const users = await fetchUsers();
const projects = await fetchProjects();
const tasks = await fetchTasks();
const departments = await fetchDepartments();
const budgets = await fetchBudgets();
// ... さらに4つ
// 合計20〜30秒(何かが間違っている)
return { users, projects, tasks, departments, budgets };
}
After:Promise.allで並列化
// ✅ 独立したAPIは並列実行(当然こうする)
async function fetchAllData() {
const [users, projects, tasks, departments, budgets] = await Promise.all([
fetchUsers(),
fetchProjects(),
fetchTasks(),
fetchDepartments(),
fetchBudgets(),
// ... 残り4つも同時に
]);
// 合計3〜5秒(これが本来の姿)
return { users, projects, tasks, departments, budgets };
}
| 指標 | Before | After | 改善率 |
|---|---|---|---|
| 処理時間 | 20〜30秒 | 3〜5秒 | 85%削減(やればできる) |
| メモリ使用量 | ベースライン | 50%削減 | 50%削減(おまけ効果) |
「なんで最初からそうしなかったの?」という声が聞こえます。はい、私もそう思います。
Step 4: データ構造の最適化でO(n²)を撲滅
O(n×m)の多重ループが8箇所あり、データ量増加とともに処理が遅延していました。「データが増えたら遅くなるのは仕方ない」と思っていた時期が私にもありました。
Before:線形探索 O(n×m)
// ❌ 8箇所の多重ループ(根性論プログラミング)
function mergeWorkWithUsers(workData: Work[], userData: User[]): MergedData[] {
return workData.map((work) => {
// 毎回全ユーザーを走査(律儀だけど遅い)
const user = userData.find((u) => u.id === work.userId);
return {
...work,
userName: user?.name ?? 'Unknown',
department: user?.department ?? 'Unknown',
};
});
}
// workData: 1000件, userData: 500件 → 50万回のループ
After:Map検索 O(n)
// ✅ Map化して検索を高速化(アルゴリズムの力)
function mergeWorkWithUsers(workData: Work[], userData: User[]): MergedData[] {
// 最初に一度だけMapを作成
const userMap = new Map(userData.map((u) => [u.id, u]));
return workData.map((work) => {
const user = userMap.get(work.userId); // O(1)で取得
return {
...work,
userName: user?.name ?? 'Unknown',
department: user?.department ?? 'Unknown',
};
});
}
// workData: 1000件, userData: 500件 → 1500回の処理
効果: データ結合処理が10秒 → 1秒未満に改善。大学で習ったアルゴリズム、ちゃんと役に立つ。(授業寝てた人、今からでも遅くない)
動作確認
パフォーマンス改善の効果を測定する方法:
// 処理時間の計測
console.time('fetchAllData');
const data = await fetchAllData();
console.timeEnd('fetchAllData');
// Next.jsのビルド出力で確認
// npm run build 時に各ページのサイズが表示される
// Route (app) Size First Load JS
// ┌ ○ / 5.2 kB 87.3 kB
// ├ ○ /dashboard 2.1 kB 84.2 kB ← Server Componentで削減
測定ツールと使い方
| ツール | 測定内容 | 使い方 |
|---|---|---|
| Lighthouse | Core Web Vitals全般 | Chrome DevTools → Lighthouse タブ |
| PageSpeed Insights | 実ユーザーデータ(CrUX) | https://pagespeed.web.dev/ |
| next build | バンドルサイズ | npm run build 実行後の出力を確認 |
| console.time() | 処理時間 | コード内に埋め込んで計測 |
| Cloud Monitoring | 本番環境のレイテンシ | GCP/Vercel ダッシュボード |
Core Web Vitals 目標値
| 指標 | 良好 | 改善が必要 | 今回の結果 |
|---|---|---|---|
| LCP | < 2.5秒 | 2.5〜4秒 | 1.8秒 ✅ |
| FID | < 100ms | 100〜300ms | 50ms ✅ |
| CLS | < 0.1 | 0.1〜0.25 | 0.05 ✅ |
Lighthouseスコアも定期的にチェック。特にTTI(Time to Interactive)とLCP(Largest Contentful Paint)の改善を確認します。
注意点・Tips
キャッシュ戦略は明示する
// 静的データ(デフォルト)
fetch(url, { cache: 'force-cache' });
// 動的データ(毎回フェッチ)
fetch(url, { cache: 'no-store' });
// ISR(定期再検証)
fetch(url, { next: { revalidate: 3600 } });
キャッシュの挙動を明示することで、「なぜ古いデータが表示される?」という問い合わせを防止。これ、書いておかないと本当に困る。3ヶ月後の自分は他人です。
エラーバウンダリは必ず設置
// app/dashboard/error.tsx
'use client';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function Error({ error, reset }: ErrorProps) {
return (
<div className="flex min-h-[400px] flex-col items-center justify-center gap-4">
<h2 className="text-xl font-semibold">エラーが発生しました</h2>
<p className="text-muted-foreground">{error.message}</p>
<button
onClick={reset}
className="bg-primary text-primary-foreground rounded-md px-4 py-2"
>
再試行
</button>
</div>
);
}
ページ単位でエラーハンドリングを設定し、部分的な障害が全体に波及しない設計に。一箇所壊れても他は動く安心感、プライスレス。
型安全なAPI設計
// lib/api.ts
import { z } from 'zod';
const MemberSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
department: z.string(),
createdAt: z.string().datetime(),
});
type Member = z.infer<typeof MemberSchema>;
export async function fetchMembers(): Promise<Member[]> {
const res = await fetch('/api/members');
if (!res.ok) {
throw new Error(`Failed to fetch members: ${res.status}`);
}
const data = await res.json();
return z.array(MemberSchema).parse(data); // ランタイムで型チェック
}
Zodでランタイムバリデーションを行い、APIレスポンスの型安全性を担保。「APIの型が変わってた」事故を未然に防ぐ。本番で気づくより、parseで落ちる方がマシ。
Next.jsが向いているケース / 向いていないケース
正直に言うと、なんでもNext.jsが正解ではありません。
向いているケース:
- データ表示がメイン → Server Componentsが活きる
- SEOが重要 → SSR/SSGでメタデータ制御が容易
- 小〜中規模チーム → フルスタック開発で効率化
- Vercel/Cloud Runでの運用 → デプロイが楽すぎて戻れない
別の選択肢を検討すべきケース:
- リアルタイム性が最重要 → WebSocket専用フレームワークを検討
- 既存のバックエンドがある → フロントのみVite/Remixも選択肢
- 非常に複雑なクライアント状態 → SPAの方が適切な場合も
まとめ
Next.js App Routerは、業務システム開発において多くのメリットをもたらします。
実案件では、API並列化で85%の高速化、データ構造最適化で10倍の速度改善といった成果が出ています。ただし、銀の弾丸ではありません。プロジェクトの特性を見極めて、適切な技術選定を。
「Next.js導入を検討している」「今のシステムが遅い」という方は、お気軽にお問い合わせください。「実際どうなの?」に本音でお答えします。
