2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js で画像のグレースケール変換を実装する — クライアントサイド vs サーバーサイドの比較

2
Posted at

はじめに

Webアプリケーションで画像をグレースケールに変換する方法は、大きく分けてクライアントサイドサーバーサイドの2つがあります。

Next.js(App Router)では両方のアプローチを柔軟に使い分けることができます。本記事では、それぞれの実装方法・メリット・デメリットを整理し、どのケースでどちらを選ぶべきかを解説します。

私自身、Grayscale Image というブラウザベースの変換ツールを開発・運用しており、その実装経験をもとにまとめました。

クライアントサイド:Canvas API による変換

基本実装

ブラウザの Canvas API を使えば、サーバーへの通信なしに画像をピクセル単位で操作できます。

// components/GrayscaleConverter.tsx
'use client';

import { useRef, useState, useCallback } from 'react';

export function GrayscaleConverter() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [converted, setConverted] = useState(false);

  const handleFileChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0];
      if (!file) return;

      const img = new Image();
      img.onload = () => {
        const canvas = canvasRef.current;
        if (!canvas) return;

        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(img, 0, 0);

        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;

        // BT.709 Luminosity 方式
        for (let i = 0; i < data.length; i += 4) {
          const gray = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
          data[i] = data[i + 1] = data[i + 2] = gray;
        }

        ctx.putImageData(imageData, 0, 0);
        setConverted(true);
      };
      img.src = URL.createObjectURL(file);
    },
    []
  );

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleFileChange} />
      <canvas ref={canvasRef} />
      {converted && <p>変換完了</p>}
    </div>
  );
}

Web Worker で UI をブロックしない

画像が大きい場合、メインスレッドでピクセル操作を行うと UI がフリーズします。Web Worker + OffscreenCanvas を使えばこの問題を回避できます。

// workers/grayscale.worker.ts
self.onmessage = (e: MessageEvent<ImageData>) => {
  const imageData = e.data;
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    const gray = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
    data[i] = data[i + 1] = data[i + 2] = gray;
  }

  // Transferable Objects でゼロコピー転送
  self.postMessage(imageData, [imageData.data.buffer]);
};
// hooks/useGrayscaleWorker.ts
'use client';

import { useRef, useCallback } from 'react';

export function useGrayscaleWorker() {
  const workerRef = useRef<Worker | null>(null);

  const convert = useCallback((imageData: ImageData): Promise<ImageData> => {
    return new Promise((resolve) => {
      if (!workerRef.current) {
        workerRef.current = new Worker(
          new URL('../workers/grayscale.worker.ts', import.meta.url)
        );
      }
      workerRef.current.onmessage = (e) => resolve(e.data);
      workerRef.current.postMessage(imageData, [imageData.data.buffer]);
    });
  }, []);

  return { convert };
}

ポイント: Transferable Objects を使うと、ArrayBuffer のコピーが発生せず、4000×3000px の画像でも転送オーバーヘッドはほぼゼロになります。

サーバーサイド:Sharp による変換

Route Handler での実装

Next.js の Route Handler(App Router)と Sharp を使えば、サーバー側で高速に変換処理を行えます。

// app/api/grayscale/route.ts
import { NextRequest, NextResponse } from 'next/server';
import sharp from 'sharp';

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const file = formData.get('image') as File;

  if (!file) {
    return NextResponse.json({ error: 'No image provided' }, { status: 400 });
  }

  const buffer = Buffer.from(await file.arrayBuffer());

  const grayscaleBuffer = await sharp(buffer)
    .grayscale()
    .toBuffer();

  return new NextResponse(grayscaleBuffer, {
    headers: {
      'Content-Type': 'image/png',
      'Content-Disposition': 'attachment; filename="grayscale.png"',
    },
  });
}

Server Action での実装

React 19 の Server Actions を使えば、フォームから直接サーバー処理を呼び出せます。

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

import sharp from 'sharp';

export async function convertToGrayscale(formData: FormData) {
  const file = formData.get('image') as File;
  if (!file) throw new Error('No image provided');

  const buffer = Buffer.from(await file.arrayBuffer());

  const result = await sharp(buffer)
    .grayscale()
    .png()
    .toBuffer();

  return `data:image/png;base64,${result.toString('base64')}`;
}
// components/ServerGrayscale.tsx
'use client';

import { useState } from 'react';
import { convertToGrayscale } from '@/app/actions/grayscale';

export function ServerGrayscale() {
  const [result, setResult] = useState<string | null>(null);

  const handleSubmit = async (formData: FormData) => {
    const dataUrl = await convertToGrayscale(formData);
    setResult(dataUrl);
  };

  return (
    <form action={handleSubmit}>
      <input type="file" name="image" accept="image/*" />
      <button type="submit">変換</button>
      {result && <img src={result} alt="Grayscale result" />}
    </form>
  );
}

比較表

観点 クライアントサイド (Canvas) サーバーサイド (Sharp)
処理速度 画像サイズに依存(大きいと遅い) 高速(C++ネイティブバインディング)
サーバー負荷 なし CPU・メモリを消費
プライバシー 画像がサーバーに送信されない アップロードが必要
対応フォーマット ブラウザが対応する形式のみ AVIF, WebP, TIFF 等も対応
バッチ処理 困難 容易
オフライン対応 可能 不可
初期ロード JSバンドルサイズ増加 サーバー依存のみ

どちらを選ぶべきか

クライアントサイドが適するケース

  • プライバシー重視: ユーザーの画像をサーバーに送信したくない場合
  • リアルタイムプレビュー: スライダーでパラメータを調整しながら変換する UI
  • オフライン対応: PWA やオフラインファーストのアプリ
  • サーバーコスト削減: 処理をすべてユーザーのブラウザに委ねる

サーバーサイドが適するケース

  • 高画質・大容量画像: 10MB以上の RAW 画像など
  • バッチ変換: 複数画像の一括処理
  • フォーマット変換: WebP → AVIF などの形式変換を同時に行う
  • APIとして提供: 外部サービスから呼び出す場合

ハイブリッドアプローチ

実際のプロダクトでは、プレビューはクライアントサイド、最終出力はサーバーサイドという組み合わせが効果的です。

// プレビュー: Canvas で即時表示
const previewGrayscale = (canvas: HTMLCanvasElement) => {
  // Canvas API でリアルタイム変換(低解像度)
};

// ダウンロード: Server Action で高品質出力
const downloadGrayscale = async (file: File) => {
  // Sharp で元解像度のまま変換
};

パフォーマンス計測

実際に 4000×3000px(12MP)の画像で計測した結果:

方式 処理時間
Canvas(メインスレッド) ~320ms
Canvas(Web Worker) ~280ms + 転送 ~2ms
Sharp(サーバー) ~45ms
Sharp + ネットワーク往復 ~45ms + RTT

Sharp のネイティブ処理は圧倒的に速いですが、ネットワーク往復を含めるとクライアントサイドの方が体感速度では優位になる場合もあります。

まとめ

Next.js App Router では、'use client''use server' を活用して、画像のグレースケール変換をクライアント・サーバーどちらでも実装できます。

  • プライバシーとレスポンスを重視するなら → Canvas API(クライアント)
  • 処理性能とフォーマット対応を重視するなら → Sharp(サーバー)
  • 最適解は両方を組み合わせたハイブリッド構成

ブラウザ上で手軽にグレースケール変換を試したい方は、grayscale image converter で各アルゴリズムの違いを比較できますので、ぜひ参考にしてみてください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?