非エンジニアがGemini+Cursorで画像圧縮サービスを作って学んだ、ブラウザ画像処理の技術的な落とし穴
はじめに:この記事の位置づけ
私はM&Aアドバイザーを本業としており、プログラミングの実務経験はありません。2026年2月20日、AIスタック(Gemini + Cursor + Claude Opus)を使って画像圧縮サービス「MamePress(マメプレス)」を48時間ほどで開発・公開しました。

言わずもがなですが、この記事は「バイブコーディングの成功談」ではありません。非エンジニアがAI駆動で開発を進める中で遭遇した、ブラウザ上の画像処理にまつわる技術的な落とし穴と、その対処法を共有するものです。
具体的には以下のトピックを扱います。
-
browser-image-compressionでPNG圧縮が効かない問題と、フォーマット自動変換のフォールバック戦略 - Canvas APIによるPNG→JPEG変換時の透過背景消失問題
-
maxWidthOrHeightと「ユーザーが指定する横幅」のギャップ - 30ファイル一括処理における並列数制御(
p-limit) - Next.js + Vercelでのセキュリティヘッダー設定
エンジニアの方々にとっては「当たり前」の内容も多いかと思いますが、AIが生成するコードにおいてこれらがどのように見落とされるか(あるいは拾われるか)という観点で、参考になる部分があれば幸いです。
開発のワークフロー:Gemini→Cursor のリレー構造
今回の開発では、Andrej Karpathy氏が2025年2月に提唱した「バイブコーディング(Vibe Coding)」に近いアプローチを取っています。ただし、純粋なバイブコーディング──コードの中身を見ずに「雰囲気」だけで進める──とは少し異なり、企画・設計フェーズにAIを活用した上で、実装もAIに任せる「2段階リレー」の構造です。
[人間] → 要件を自然言語で伝える
↓
[Gemini] → 市場分析・技術選定・アーキテクチャ設計
↓ Cursor用のプロンプト(指示文)を生成
[Cursor + Claude Opus] → コーディング・デバッグ
↓
[人間] → 動作確認・フィードバック → [Gemini]に戻る
Geminiが生成したCursor用プロンプトの冒頭は、こんな感じでした。
あなたはNext.js、TypeScript、Tailwind CSSのエキスパートであるシニアフロントエンド
エンジニアです。以下の要件に基づき、日本市場向けの画像圧縮WebサービスのMVP
(実用最小限の製品)を構築してください。
このあとに技術スタック、ライブラリ選定、機能要件、実装ステップが構造化されて続きます。Geminiは実装を4ステップに分割し、「一度に全部を指示するとAIがロジックを混乱させるリスクがある」と事前に警告してきました。
2026年2月現在、Karpathy氏自身が「バイブコーディングの次」として「エージェンティックエンジニアリング(Agentic Engineering)」──人間が設計・評価の上位工程を担い、複数AIエージェントを統制するスタイル──を提唱し始めています。今回のGemini→Cursorリレーは、図らずもその方向に近い構造になっていたのかもしれません。
落とし穴1:browser-image-compressionでPNGが縮まない
問題
browser-image-compressionは、ブラウザ上で画像圧縮を行うためのライブラリです。maxSizeMBオプションを指定すると、目標サイズに向けて画質を段階的に下げてくれます。
const options = {
maxSizeMB: 0.15, // 150KB
maxWidthOrHeight: 1920,
useWebWorker: true,
};
const compressedFile = await imageCompression(file, options);
JPEGの場合はこれで十分に機能します。しかし、PNG画像ではファイルサイズがほとんど変わらないことがあります。
原因
TinyPNGが高い圧縮率を実現できるのは、PNG画像を24bit(フルカラー:約1677万色)から8bit(256色)に減色する量子化処理(pngquant相当)を行っているからです。
一方、browser-image-compressionはブラウザのCanvas APIを使って画像を再描画・書き出ししているだけなので、PNGに対する高度な減色処理は行われません。Canvas APIのtoBlob()メソッドでPNGを書き出すと、ロスレス(可逆圧縮)のまま出力されるため、ファイルサイズの削減効果が限定的です。
Canvas API → toBlob('image/png') → ロスレス圧縮のまま → サイズ変わらず
TinyPNG → pngquant(24bit→8bit減色)→ 劇的にサイズ削減
対処:3段階フォールバック戦略
Geminiとの壁打ちの中で出てきた対処法は、出力フォーマットを自動変換するというアプローチでした。
1. 元の形式(PNG)で圧縮を試みる
2. WebPに変換して圧縮を試みる
3. JPEGに変換して圧縮を試みる
→ 3つの結果を比較し、最も軽いものを採用
WebP(Google開発の次世代フォーマット)は同等画質でJPEGより25-35%程度軽くなる傾向があり、PNG画像の圧縮においても有効な選択肢です。
ただし、この自動変換にはビジネス要件との衝突リスクがあります(後述)。
落とし穴2:フォーマット自動変換がビジネス要件と衝突する
問題
フォールバック戦略で最軽量のフォーマットを自動選択する設計は、技術的には合理的です。しかし、広告入稿の現場では「.pngまたは.jpgのみ」という規定がまだ多いという事実があります。
ユーザーがPNGをアップロードして、知らないうちにWebPで返ってきたら──入稿時にエラーになります。
対処:ユーザー選択制
最終的に、出力フォーマットを以下の4つから選択できるUIにしました。
| モード | 挙動 |
|---|---|
| 推奨(自動) | PNG→WebP→JPEGのフォールバックで最軽量を自動選択 |
| 元の形式を維持 | 入力フォーマットのまま圧縮。変換試行はスキップ |
| WebPに変換 | すべての画像をimage/webpで出力 |
| JPEGに変換 | すべての画像をimage/jpegで出力 |
デフォルトは「推奨(自動)」ですが、入稿規定がある場合は「元の形式を維持」を選べるようにしています。
落とし穴3:PNG→JPEG変換で透過背景が消える
問題
透過背景を持つPNG画像をJPEGに変換すると、透明部分が真っ黒になります。これはJPEGフォーマットがアルファチャンネル(透過情報)をサポートしていないためです。
Canvas APIで透過PNGを描画し、toBlob('image/jpeg')で書き出すと、透過部分はCanvasのデフォルト背景色(黒)で塗りつぶされます。
対処
JPEG変換(およびフォールバック戦略でJPEGが選択される場合)には、Canvas描画前に白色(#FFFFFF)で塗りつぶす前処理が必要です。
// Canvas APIで白背景を先に描画してからJPEGに変換する擬似コード
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
// 白背景で塗りつぶし(これがないと透過部分が黒になる)
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 元画像を上に描画
ctx.drawImage(img, 0, 0);
// JPEG形式でBlobに変換
canvas.toBlob((blob) => { /* ... */ }, 'image/jpeg', quality);
Geminiはこの問題を「エンジニアが忘れがちなポイント」として事前にCursor用の指示文に含めていました。ECサイトの商品画像(白背景必須)などのユースケースを想定すると、この前処理は必須です。
落とし穴4:maxWidthOrHeightと「ユーザーが指定する横幅」のズレ
問題
browser-image-compressionのmaxWidthOrHeightオプションは、名前の通り長辺の最大値を指定するパラメータです。
const options = {
maxWidthOrHeight: 1200, // 長辺が1200px以下になるようリサイズ
};
しかし、ユーザーが「横幅1200pxにリサイズしてほしい」と考えた場合、縦長画像(例:3000×4000px)では期待通りの結果になりません。
入力: 3000 × 4000px(縦長)
ユーザーの期待: 横幅 = 1200px → 出力: 1200 × 1600px
実際の挙動: 長辺(高さ) = 1200px → 出力: 900 × 1200px
→ 横幅が1200pxではなく900pxになる
対処:動的なmaxWidthOrHeight計算
ユーザーが指定した「横幅」を実現するために、画像のアスペクト比に応じてmaxWidthOrHeightの値を動的に計算するロジックが必要です。
// ユーザー指定の横幅から、maxWidthOrHeightを動的計算する擬似コード
function calcMaxDimension(
originalWidth: number,
originalHeight: number,
targetWidth: number
): number {
if (originalWidth >= originalHeight) {
// 横長画像:横幅 = 長辺なのでそのまま
return targetWidth;
} else {
// 縦長画像:長辺(高さ)を逆算
return Math.round(targetWidth * (originalHeight / originalWidth));
}
}
// 使用例
const maxDim = calcMaxDimension(3000, 4000, 1200);
// → Math.round(1200 * (4000 / 3000)) = 1600
// maxWidthOrHeight: 1600 → 出力: 1200 × 1600px(横幅がぴったり1200px)
このロジックにより、縦長のスマートフォン写真でも「指定した横幅」でリサイズされるようになります。
落とし穴5:リサイズと圧縮の処理順序
問題
「横幅1200pxにリサイズ」と「150KB以下に圧縮」を同時に指定した場合、処理順序によって結果が大きく変わります。
正しい順序:リサイズ → 圧縮
5MB (3000px) → [リサイズ] → 500KB (1200px) → [圧縮] → 150KB (1200px)
↑ 150KBの"予算"を1200px画像のためにフルに使える
間違った順序:圧縮 → リサイズ
5MB (3000px) → [圧縮] → 150KB (3000px, 画質ボロボロ) → [リサイズ] → 30KB (1200px)
↑ 画質は悪い上に、150KBよりはるかに小さい
browser-image-compressionでは、maxSizeMBとmaxWidthOrHeightを同時に指定した場合、内部的に「まず画質を下げ、それでも目標に達しなければ解像度を縮小する」という順序で処理されます。つまり、リサイズ値を事前に計算してmaxWidthOrHeightに渡しておけば、ライブラリ側で適切な順序で処理が行われます。
落とし穴6:30ファイル一括処理でブラウザがフリーズする
問題
マメプレスでは一度に30ファイルまでの一括処理に対応しています(TinyPNG無料版の20ファイル制限への差別化)。しかし、30ファイルの圧縮をPromise.all()で一斉に開始すると、ブラウザのメインスレッドが占有されてUIが固まります。
対処:p-limitによる並列数制御
p-limitを使って、同時処理数を制限します。
import pLimit from 'p-limit';
const limit = pLimit(3); // 同時実行数を3に制限
const compressAll = async (files: File[]) => {
const tasks = files.map((file) =>
limit(() => compressImage(file))
);
return Promise.all(tasks);
};
同時処理数を3に設定した根拠は、browser-image-compressionのuseWebWorker: trueオプションとの兼ね合いです。Web Workerを使ったマルチスレッド処理でも、Canvas APIの描画自体はメインスレッドに戻ってくるため、あまり多くの並列処理を走らせるとUIがカクつきます。3並列は、体感的なレスポンスとスループットのバランスが取れるラインでした。
セキュリティ:クライアントサイド処理の利点とNext.jsでの追加設定
マメプレスはすべての処理がブラウザ内で完結する設計です。画像データがサーバーに送信されることはありません。これはセキュリティ上の最大の利点ですが、それだけでは不十分な部分もあります。
Next.jsのセキュリティヘッダー
next.config.mjsにセキュリティヘッダーを追加することで、一般的なWeb攻撃への耐性を強化できます。
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY', // iframe埋め込みによるクリックジャッキングを防止
},
{
key: 'X-Content-Type-Options',
value: 'nosniff', // MIMEタイプスニッフィングを防止
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
export default nextConfig;
XSS耐性
ファイル名に悪意あるスクリプトを仕込んだ画像(例:<script>alert('xss')</script>.jpg)をアップロードされる可能性がありますが、React(Next.js)はJSX内でテキストを表示する際に自動でエスケープ処理を行うため、デフォルトで耐性があるようです。ただし、dangerouslySetInnerHTMLを使っている箇所がないかは要確認です。
環境変数がゼロという利点
マメプレスにはデータベース接続情報も外部APIキーも不要です。唯一の環境変数はGoogle Analytics用のNEXT_PUBLIC_GA_IDだけ。.env.localの管理漏れでサービスが止まるリスクが構造的に存在しないようです。
AIが生成したコードを「AIにレビューさせる」ことの限界
開発の終盤で、Geminiに公開URLを共有してセキュリティチェックを依頼しました。Geminiの評価は「非常に安全性の高いWebアプリケーション」というものでした。
しかし、ここには構造的な問題があります。
[Gemini] → 設計・技術選定
↓
[Cursor/Claude] → コード生成
↓
[Gemini] → セキュリティレビュー ← AIが作ったものをAIがレビューしている
AIによるセキュリティレビューが、プロのペネトレーションテストと同等の品質を持つかどうかは判断できません。特に、ブラックボックス的な外部スキャンだけでは検出できない脆弱性──ロジックの欠陥やレースコンディション──は見落とされる可能性があります。
バイブコーディングで作ったサービスを本番運用する場合、素人ながらコードレビューやセキュリティ監査を外部の人間に依頼するステップは省略すべきではないと考えています。
Gemini→Cursorプロンプトの設計パターン
Geminiが生成したCursor用プロンプトには、いくつかの共通パターンがありました。エンジニアがAIコーディングツールにプロンプトを書く際にも参考になるかもしれません。
1. ロールの明示
あなたはNext.js、TypeScript、Tailwind CSSのエキスパートである
シニアフロントエンドエンジニアです。
2. 段階的な実装指示(Step分割)
Step 1: プロジェクトの土台とライブラリのインストール
Step 2: 画像圧縮ロジック(カスタムフック)の実装
Step 3: UIコンポーネントの構築
Step 4: 全体の統合と動作確認
3. 「やってはいけないこと」の明示
30枚を一斉に処理開始してはいけません。
p-limitなどのライブラリを使って並列数を3に制限してください。
4. ビジネス要件との接続
PNG→JPEG変換時は、背景を白(#FFFFFF)で塗りつぶしてください。
ECサイトの商品画像で白背景が必須になるケースへの対応です。
ロールの設定、段階分割、禁止事項の明示、「なぜそうするか」の理由付け──このあたりは人間がプロンプトを書く際にも有効な構造だと感じています。
技術スタック一覧
| カテゴリ | ツール / ライブラリ | 役割 |
|---|---|---|
| フレームワーク | Next.js (App Router) | SSG + クライアントサイド処理 |
| 言語 | TypeScript | 型安全な開発 |
| UI | Tailwind CSS + shadcn/ui | スタイリング + UIコンポーネント |
| 画像圧縮 | browser-image-compression | ブラウザ内圧縮エンジン |
| ファイル入力 | react-dropzone | ドラッグ&ドロップUI |
| ZIP生成 | jszip | 一括ダウンロード |
| 並列制御 | p-limit | 同時処理数の制限 |
| UX演出 | canvas-confetti | 圧縮完了時の紙吹雪 |
| ホスティング | Vercel | デプロイ + CDN + HTTPS |
| 計測 | Google Analytics 4 | アクセス解析 |
| 企画・設計 | Google Gemini | 市場分析・技術選定・プロンプト生成 |
| 実装 | Cursor + Claude Opus | コーディング・デバッグ |
まとめ:バイブコーディングで「見えなかった問題」が見えた
今回の開発を通じて実感したのは、AIは「動くコード」を生成するのは得意だが、「ビジネス要件と技術仕様の間のギャップ」は人間が埋める必要があるということです。
特に画像処理の領域では、
- Canvas APIの制約(PNGの減色処理ができない、透過背景の扱い)
- ライブラリのパラメータの意味と、ユーザーの期待値のズレ(
maxWidthOrHeight≠ 横幅指定) - 広告入稿規定のようなドメイン固有の制約
──これらはAIだけでは検出しにくい問題です。Geminiが「壁打ち相手」として機能したのは、私がM&Aアドバイザーとしての業務知識(広告入稿のファイルサイズ規定など)をフィードバックしたからであり、AIに丸投げしていたら見落としていた問題がいくつもあります。
コードの品質やセキュリティについては、正直なところ検証する能力が私にはありません。エンジニアの方々からのフィードバックをいただけると非常に助かります。MamePressは実際に公開されていますので、触ってみて気になった点があればコメントやXでお知らせください。
関連リンク
- MamePress(マメプレス) - 今回作ったサービス
- browser-image-compression (GitHub) - 使用した圧縮ライブラリ
- TinyPNG - 比較対象としたサービス
- Google Squoosh - Googleのブラウザベース画像圧縮ツール
Discussion