playpark
ホーム会社概要サービスソリューションブログお知らせお問い合わせ
playpark

あらゆる仕事を楽しむ

会社概要サービスソリューションお問い合わせ特定商取引法に基づく表記

© 2019-2026 合同会社playpark All Rights Reserved.

  1. ホーム
  2. ブログ
  3. 技術Tips
  4. 【Next.js App Router】実案件でパフォーマンス85%改善した技術選定
ブログ一覧に戻る
技術Tips

【Next.js App Router】実案件でパフォーマンス85%改善した技術選定

複数の業務システム開発を通じて得られた、Next.js App Router選定とパフォーマンス最適化の知見。実際の改善事例とともに解説します。

2026年1月15日26分で読める
Next.jsReactTypeScriptフロントエンドパフォーマンス最適化
【Next.js App Router】実案件でパフォーマンス85%改善した技術選定

「Server Componentsがすごいらしいけど、結局どう使えばいいの...?」

Next.js App Router、公式ドキュメント読んでも「で、うちのプロジェクトではどうすれば」がわからない。その気持ち、わかります。

playparkでは複数の業務システムをNext.js + TypeScriptで構築してきました。本記事では、マーケティング記事ではなく、実際のプロジェクトから得られた本音の知見を共有します。(キラキラした成功談だけじゃなく、「最初からそうしろよ」案件も含めて)

実績サマリー:この記事の根拠データ

改善項目BeforeAfter改善率測定方法
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 };
}
指標BeforeAfter改善率
処理時間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で削減

測定ツールと使い方

ツール測定内容使い方
LighthouseCore 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< 100ms100〜300ms50ms ✅
CLS< 0.10.1〜0.250.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導入を検討している」「今のシステムが遅い」という方は、お気軽にお問い合わせください。「実際どうなの?」に本音でお答えします。

→ お問い合わせはこちら

About playpark

「あらゆる仕事を楽しむ」をミッションに、業務自動化・AI活用を手がける合同会社です。このブログの運用自体も、自社開発のClaude Code Skillsで完全自動化しています。

会社概要サービス
一緒に実験しませんか?
ブログ一覧に戻る

この技術を活用したサービス

記事の技術を使って、業務課題を解決しませんか?playparkのソリューションをご覧ください。

ソリューション一覧へ