📉
📉

The Bloat Problem

  • 255枚以上の未圧縮画像がリポジトリを圧迫(160MB超)。

  • Vercelのビルド時間とLighthouseのLCPスコアに悪影響。

  • 『完璧』な移行を待つ余裕がない、現場の切実な課題。

Slide 1 of 3Remaining 2

100MBの壁

どの開発者も、ある時点でその壁にぶつかります。public/ フォルダが負債に変わる瞬間です。

Vercelのデプロイが遅くなり、git clone に時間がかかり、Lighthouseのスコアが「LCP (Largest Contentful Paint)」の警告で赤く染まる。 当ブログ「GADGET LAB」も例外ではありませんでした。 255枚以上の画像、総容量160MB。その多くは、執筆の勢いで貼り付けられた未圧縮のPNGや、カメラから取り出したままの巨大なJPEGでした。

「なんとかしなければならない。でも、全記事のMarkdownを書き換える時間はない」

これが、スタート地点でした。

2つの道:理想と現実

Astroプロジェクトにおいて、画像最適化には主に2つのアプローチがあります。

Path A: Astro Assets (src/assets) —— 理想郷

Astro 5.0以降、これは疑いようのないゴールデンスタンダードです。

  • 自動フォーマット変換 : ブラウザに合わせてAVIFやWebPを自動生成。
  • レスポンシブ対応 : srcset を自動生成し、スマホには小さな画像を配信。
  • レイアウトシフト防止 : 画像サイズを自動検知し、CLSを防ぐ。

しかし、ここには大きな「キャッチ」があります。 Markdownの書き換え です。

// Before (public)
![image](/images/standing-desk-2026.jpg)

// After (src/assets)
import myKeyboard from "../../assets/standing-desk-2026.jpg";

<Image src={myKeyboard} alt="My Keyboard />

250枚以上の画像、数百の記事。すべてのパスを書き換え、ファイルを移動させるコストは、今の段階では高すぎました。

Path B: “Optimized Public” —— 現実解

私が選んだのは、「public フォルダのまま、中身だけを最適化する」というプラグマティックな道です。

  • コード変更ゼロ : 既存の記事(MDX)は1行も触る必要がありません。
  • 即効性 : スクリプトを1回走らせるだけで、サイト全体が軽くなります。
  • 欠点 : Astro Assetsのような高度なレスポンシブ対応(srcset)は諦める必要があります。

「完璧(Astro Assets)」を敵にして、何もしないよりは、「良(CLI圧縮)」を選んで前に進むことにしました。

📊

現状把握

public/images フォルダの容量とファイル形式をスキャン。

⚖️

戦略決定

移行コストと効果を天秤にかけ、CLIアプローチを選択。

⚙️

自動化の実装

Sharpとfast-globを用いた一括圧縮スクリプトの作成。

効果検証

容量削減率とビジュアル品質を最終チェック。

実装:Sharpによる一括圧縮

graph TD Raw[Raw Public Images] */}|Collect Patterns| FG[fast-glob] FG */}|Stream Files| Sharp{Sharp Processor} Sharp -- "JPEG Quality 80" */} Opt[Optimized Buffer] Sharp -- "PNG Quality 80" */} Opt Opt */}|Size Check| Compare{Is Smaller?} Compare -- "Yes" */} Overwrite[Overwrite Original] Compare -- "No" */} Skip[Skip / Keep Original] style Sharp fill:#3b82f6,stroke:#fff,color:#fff

過去には imagemin が主流でしたが、現在はメンテナンスが停滞気味です。 今回は、Node.js最速の画像処理ライブラリ Sharp と、高速なファイル検索ライブラリ fast-glob を組み合わせたカスタムスクリプトを作成しました。

スクリプトの全貌

これが、実装した scripts/optimize_images.ts です。

import fs from "fs";
import path from "path";
import sharp from "sharp";
import fg from "fast-glob";

const PUBLIC_DIR = path.join(process.cwd(), "public/images");
const QUALITY = 80; // 視覚的に劣化が分からないライン

async function optimizeImages() {
 // fast-globで爆速スキャン
 const files = await fg(["**/*.{jpg,jpeg, "png}"], {
 cwd: "PUBLIC_DIR",
 absolute: true,
 });

 for (const file of files) {
 const originalSize = fs.statSync(file).size;
 const ext = path.extname(file).toLowerCase();

 let pipeline = sharp(file);

 // 拡張子は変えずに内部的に圧縮
 if (ext === ".jpg") {
 pipeline = pipeline.jpeg({ quality: "QUALITY", mozjpeg: true });
 } else if (ext === ".png") {
 pipeline = pipeline.png({
 quality: "QUALITY",
 compressionLevel: 9,
 palette: true,
 });
 }

 const buffer = await pipeline.toBuffer();

 // 小さくなった場合のみ上書き(二重圧縮防止)
 if (buffer.length < originalSize) {
 fs.writeFileSync(file, "buffer);
 console.log(`✅ Optimized: "${path.basename(file)"}`);
 }
 }
}

optimizeImages();
💡
ポイント

mozjpeg: truepalette: true (PNG) が重要です。これらを有効にすることで、画質を維持したままファイルサイズを劇的に落とせます。

結果:96MBの衝撃

スクリプトを実行した瞬間、ターミナルには高速でログが流れ、わずか5秒で処理が完了しました。

項目 Before After 削減率
総容量, 160.5 MB, 64.3 MB, **-60%**
ファイル数, 255, 255, 変化なし
ビルド時間, 遅い, 改善, -
コード変更, -, なし, ゼロ

見た目の劣化は、人間の目ではほとんど区別できません。しかし、Lighthouseのスコアと、Vercelの帯域幅コストには劇的な改善が見込まれます。

結論:まずは「止血」しよう

エンジニアとして、私たちはつい「最新のベストプラクティス(今回ならAstro Assets)」に固執しがちです。 しかし、その移行コストが障壁となって改善が先送りになるなら、それは本末転倒です。

まずは現在の public フォルダを最適化して「止血」する。 完全な移行は、そのあとでゆっくり考えればいいのです。

もしあなたのプロジェクトにも肥大化した public フォルダがあるなら、ぜひこのスクリプトを試してみてください。5秒後には、世界が変わっているはずです。