ant.seal.dev

ClaudeCodeが作った画像一覧が重すぎたので、サーバーコンポーネント+サーバーアクションにした話

2025年07月28日

背景

ClaudeCodeを使って生成した画像一覧を、Next.jsのクライアントコンポーネントで表示していたところ、以下のような課題が発生しました。

  • ページ読み込みが遅い
  • メモリ使用量が高い
  • 更新・削除アクション時に毎回状態管理が面倒

課題

当初はクライアントコンポーネント側で以下のような処理を行っていました。

  • APIから画像一覧をfetch
  • useState useEffectで状態管理
  • 削除・更新ボタンもクライアント側で処理

結果として、画像が増えるほどパフォーマンスが悪化。

改善方針

  • 画像一覧の取得 → サーバーコンポーネントで実行
  • 更新・削除操作 → App Routerのサーバーアクションで対応

実装

サーバーコンポーネントで画像一覧取得

// app/images/page.tsx  
import { getAllImages } from '@/lib/image';

export default async function ImageListPage() {
  const images = await getAllImages();

  return (
    <div>
      {images.map((img) => (
        <div key={img.id}>
          <img src={img.url} alt={img.name} />
          {/* ボタンは後述 */}
        </div>
      ))}
    </div>
  );
}

サーバーアクションで削除・更新

// app/images/actions.ts  
'use server';

import { deleteImage, updateImage } from '@/lib/image';

export async function deleteImageAction(id: string) {
  await deleteImage(id);
}

export async function updateImageAction(id: string, newData: any) {
  await updateImage(id, newData);
}

ボタンとの連携(Client Component)

// app/images/DeleteButton.tsx  
'use client';

import { deleteImageAction } from './actions';

export function DeleteButton({ id }: { id: string }) {
  const handleDelete = async () => {
    await deleteImageAction(id);
  };

  return <button onClick={handleDelete}>削除</button>;
}

呼び出し側でボタンを使う

// app/images/page.tsx 内の一部  
import { DeleteButton } from './DeleteButton';

{images.map((img) => (
  <div key={img.id}>
    <img src={img.url} alt={img.name} />
    <DeleteButton id={img.id} />
  </div>
))}

効果

  • 初回レンダリングが高速化:CSR → SSRへの移行で不要なJSロジックを排除
  • 状態管理がシンプルに:クライアントでのuseState/useEffectが不要に
  • UXが安定:ページ遷移や再描画をSSRで一貫して処理可能に

まとめ

サーバーアクションと組み合わせることで、以下を両立できます:

  • ユーザー体験の改善(表示が速い)
  • コードの可読性・保守性の向上
  • クライアント側の状態管理の削減

また、サーバーコンポーネントの更新にはrevalidatePathを使いました


同じように画像一覧や重いデータを扱っていて、パフォーマンスや状態管理に悩んでいる方の参考になれば幸いです。