gazou-compressor.jp

LQIPプレースホルダ設計:blurDataURL + 支配色で“先に見える”を最小コストで

LQIPは画像本体の前に“雰囲気の見え”を置くテクニック。空白が消えるため体感が大きく改善します。 ただし動的生成過剰なデータ量はINPを悪化させることがあります。 本稿は「軽い・単純・静的」を合言葉に、運用で事故りにくい設計をまとめます。

先に結論
  • 24px/Q=40の小さなJPEG支配色背景の併用が最もコスパ良。
  • プレースホルダは前処理でJSON化し、CSR計算を避ける。
  • CLSゼロは width/heightsizes、レイアウトは content-visibility で安定化。

要点(TL;DR)

1. LQIPが効く理由と効かない条件

人は先に“全体感”を把握できると待ち時間のストレスが減ります。LQIPはこれを満たす簡易手段です。 一方、透明度が高いPNGやアイコンでは境界が崩れやすく、逆に違和感が出ることがあります。

2. 最短フロー(実務)

  1. 素材から24pxのミニサムネを生成し、base64化。
  2. 同時に支配色を抽出し、背景色に設定。
  3. JSONに集約してNext.jsからimport、placeholder="blur"で使用。
  4. ページ毎の総量を把握し、必要なら幅/品質を落としてチューニング。

3. 実装レシピ(コピペOK)

3.1 生成スクリプト

// scripts/gen-blur-placeholder.ts
// 依存: npm i -D sharp tsx
// 出力: placeholders.json({ "filename": { blurDataURL, bg, bytes } })
import fs from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";

const IN = process.env.IN ?? "input";
const OUT = process.env.OUT ?? "placeholders.json";

async function main() {
  const files = (await fs.readdir(IN)).filter(f => /\.(jpe?g|png|webp|avif)$/i.test(f));
  const map: Record<string, { blurDataURL: string; bg: string; bytes: number }> = {};

  for (const f of files) {
    const src = path.join(IN, f);

    const blur = await sharp(src)
      .resize({ width: 24 })               // 16〜32pxでOK。風景は16でも可
      .jpeg({ quality: 40, mozjpeg: true }) // 30〜50で調整
      .toBuffer();

    const { dominant } = await sharp(src).stats();
    const bg = `rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`;

    map[f] = {
      blurDataURL: "data:image/jpeg;base64," + blur.toString("base64"),
      bg,
      bytes: blur.byteLength
    };
  }

  const total = Object.values(map).reduce((s, v) => s + v.bytes, 0);
  console.log("Placeholders total:", Math.round(total/1024), "KB");

  await fs.writeFile(OUT, JSON.stringify(map, null, 2), "utf-8");
  console.log("→", OUT);
}
main().catch(e => { console.error(e); process.exit(1); });

3.2 使用例(next/image)

// app/(example)/ImageCard.tsx
import Image from "next/image";
import placeholders from "@/placeholders.json"; // 生成物をimport

export default function ImageCard({ src, w, h }: { src: string; w: number; h: number }) {
  const ph = (placeholders as any)[src];
  return (
    <div className="rounded-xl overflow-hidden" style={{ background: ph?.bg ?? "#f2f2f2" }}>
      <Image
        src={"/img/" + src}
        alt=""
        width={w}
        height={h}
        placeholder={ph ? "blur" : "empty"}
        blurDataURL={ph?.blurDataURL}
        sizes="(min-width: 1024px) 600px, 100vw"
        loading="lazy"
      />
    </div>
  );
}

3.3 package.json(ビルド前生成)

// package.json(抜粋:ビルド前にLQIPを生成)
{
  "devDependencies": { "sharp": "^0.33.0", "tsx": "^4.0.0" },
  "scripts": {
    "lqip:gen": "tsx scripts/gen-blur-placeholder.ts",
    "build": "npm run lqip:gen && next build"
  }
}

3.4 比較メモ(BlurHash/SVG/CSS)

3.5 CSS(レイアウト安定化)

/* content-visibility で初期レイアウトを安定化 */
.cv-auto { content-visibility: auto; contain-intrinsic-size: 1 300px; }

4. 応用と使いどころ

5. 計測と検証(Lighthouse/Field)

// (任意)LQIPの効果をGA4にイベント送信する例(擬似コード)
import { getCLS } from "web-vitals/attribution";
getCLS((m) => {
  // 画像読み込み中にCLSが出ていないか観測
  window.gtag?.("event", "lqip_cls", { value: Math.round(m.value * 1000) });
});

サイズの目安(例)

  • カード12枚のサムネ:24px/Q=40 → プレースホルダ合計 ~10–18KB
  • ヒーロー1枚:24px/Q=40 → ~1–3KB
  • 合計 15KB 前後なら体感は十分向上し、初期転送への影響も小さいことが多いです。

6. アクセシビリティ / ダークモード

7. ありがちなNGと回避策

8. 公開前チェック

9. まとめ

LQIPは前処理で静的化が正解。最小の追加バイトで、待ち時間のストレスを確実に減らせます。 画像自体の設計は Progressive JPEG、 配信は キャッシュ設計 と合わせて総合最適へ。

10. FAQ

BlurHashやSVGトレースは使わないほうがいい?
プロジェクト次第です。CSR復号/描画コストと引き換えに表現力が増します。INPに敏感なら静的LQIPが扱いやすいです。
LQIPはSEOに影響する?
画像コンテンツ自体の評価には直接関係しませんが、体感速度の改善は離脱率低下に寄与します。適切なaltや 構造化データと合わせて品質を高めましょう。

公開:2025-09-03

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事