画像をWebP化したら75MBが6.6MBになった|移行手順と落とし穴

画像をWebP化したら75MBが6.6MBになった|移行手順と落とし穴

2026年9月4日に、このブログの画像を全部 WebP に入れ替えました。289ファイル・75.69MB が 6.61MB になり、91.3%減です。ただ、そのあと「WebP にしていなかった1枚」が原因で X のリンクカードが壊れる事故を起こしました。

この記事は、一般論としての画像最適化ではなく、自分のサイトで実際にやった移行の記録と、そこで踏んだ落とし穴です。数字はすべて wadokon.com の実データです。

目次

移行前|PNGが289ファイル・75.7MBあった

このブログのアイキャッチは、HTMLを書いてヘッドレスChromeでスクリーンショットを撮る方法で作っています。--screenshot の出力は PNG なので、何も考えないと PNG がそのまま本番に上がります。それを続けた結果が次の状態でした。

  • PNG 289ファイル(WordPressが自動生成するサムネイル込み)で 75.69MB
  • フルサイズのアイキャッチ1枚が 814〜1,101KB

写真ベースの画像を PNG で持つのは、可逆圧縮に写真を通しているだけなので単純に無駄です。1MB近いアイキャッチが記事一覧に何枚も並ぶ状態で、ここを直さずに他のチューニングをする意味はありませんでした。

形式選び|同じ1枚を4形式で保存して比べた

WebP と AVIF のどちらにするか決めるため、実際に使っているアイキャッチ(1200×675 の写真ベース)を同じ原本から4形式に保存し直しました。

PNG 842KB・JPEG 103KB・WebP 60KB・AVIF 44KB の横棒グラフ
このブログで実際に使っているアイキャッチ1枚を、Pillow 12.2.0 で4形式に保存し直した結果
形式サイズ原本比
PNG(原本)842 KB100%
JPEG q84103 KB12.2%
WebP q8460 KB7.1%
AVIF q6544 KB5.2%

数字だけ見れば AVIF が最小です。それでも WebP で統一しました。理由は、このサイトの画像がほぼ全部 og:image としてSNSのクローラーに読まれるからです。ブラウザの対応状況だけで決められない領域があり、そこで冒険する理由がありませんでした。この判断が正しかったことは、3日後に別の形で証明されます。

変換は Pillow で一括処理しました。写真ベースなら q84 で目視の劣化はほぼ分かりません。

from PIL import Image
Image.open(src).convert('RGB').save(dst, 'WEBP', quality=84, method=6)

method=6 は圧縮にいちばん時間をかける設定です。変換は1回きりで配信は何万回もあるので、ここをケチる理由はありません。

移行結果|75.69MB → 6.61MB(91.3%減)

同名で PNG と WebP が揃っている289ファイルを突き合わせた結果です。

対象PNGWebP削減
全289ファイル(サムネ込み)75.69 MB6.61 MB91.3%
フルサイズ60枚のみ33.1 MB2.5 MB92.5%

ここで1つ、意図的にやっていないことがあります。元のPNGを削除していません。Google画像検索が旧PNGのURLをインデックスしているため、いま消すと画像側が404になるからです。サーバー容量には余裕があるので、Search Console の画像インデックスが WebP に移るのを確認してから、2026年12月以降に消す予定にしています。バックアップも別ディレクトリに退避済みです。

「置き換えたら即消す」は事故のもとです。配信を切り替えるのと、旧ファイルを消すのは、別のタイミングでやるのが安全でした。

3日後の事故|XがJPEGのアイキャッチを採用しなかった

9月6日に記事を1本公開し、X に投稿したところ、リンクカードが画像なしの小さいサマリー表示になりました。

調べた範囲では、こちら側に問題は見つかりませんでした。

  • HTML の og:image は正しく出力されている
  • クローラーのUAで取得しても同じ結果
  • 画像URL は 200 で返り、サイズも 1200×675 で要件を満たしている

Cloudflare のログを引いて、ようやく状況が見えました。Twitterbot は投稿直後からその画像を約30秒おきに170回以上、毎回200で全バイト取得していました。取得できていないのではなく、取得したうえで採用していなかったわけです。

違いは形式だけでした。その1枚だけが JPEG で、同じサイトの WebP のアイキャッチは1回の取得で正常に表示されていました。ファイル自体はベースライン 4:2:2 の素直な JPEG で、壊れてはいません。

原因は自分の手順書でした。9月4日に全画像を WebP にしたのに、アイキャッチ生成の手順書に「JPEGに変換する」という古い記述が残っていたのです。移行したのはファイルだけで、手順を直していませんでした。

同じURLで再投稿しても直らない

画像を WebP に差し替えて X に投稿し直しましたが、カードは直りませんでした。Cloudflare のログを見ると、X は再投稿時にHTMLは取り直しているのに、新しい og:image を一度も取りに来ていません。URL単位のカードキャッシュを再利用し、旧JPEGを取り続けていました。

対処法は2つだけです。

  1. クエリを付けて別URLとして投稿する(https://example.com/slug/?v=2)
  2. X側のキャッシュが切れるのを待つ

つまりog:image の確認は、投稿する前にやらないと取り返しがつきません。公開直後に必ず実行するようにしたのがこれです。

curl -s -A "Twitterbot/1.0" https://example.com/slug/ \
  | grep -oE '<meta property="og:image" content="[^"]*"'
# → .webp のURLであることを確認し、続けてそのURLを GET して 200 を確認する
リサイクル工場の圧縮機が段ボールを立方体に固めている写真。横に完成したベールがある
中身を変えずに、かさだけ減らす

差し替えは必ず新しいファイル名で

同名で上書きすると、CDNのエッジキャッシュ(このサイトは Cloudflare で7日)とSNS側のキャッシュに残った旧ファイルが使われ続けます。差し替えるときは eyecatch-slug-v2.webp のように新しいファイル名で上げるのが結局いちばん速い、というのが今回の結論です。

自己監査したら、まだJPEGが18枚残っていた

この記事を書くにあたって、サイト全体をスキャンし直しました。結果は次のとおりです。

  • 記事の本文画像 13枚が JPEG のまま
  • 固定ページのスクリーンショット 2枚が JPEG のまま
  • アイキャッチ 3枚が JPEG のまま

合計18枚です。全部 WebP にしたつもりでいましたが、実際には全然できていませんでした。

特にまずかったのが3枚目のアイキャッチです。これは公開予約中の記事のもので、予約日時になれば自動的に公開され、そのまま X に流していたはずでした。数日前に起こした事故と、まったく同じ条件が仕込まれていたことになります。原因も同じで、手順書を直す前に作った画像がそのまま残っていました。

実際にページが読み込んでいる画像を大きい順に並べると、状況がはっきりします。

ファイル形式転送量
本文画像(最大のもの)JPEG283 KB
本文画像(2番目)JPEG217 KB
アイキャッチWebP40 KB

いちばん軽いのが、いちばん目立つアイキャッチという逆転が起きていました。しかも WordPress は元画像と同じ形式でサムネイルを生成するので、JPEGを1枚上げると、そのサイズ違いが3〜4枚まとめてJPEGで増えます。1枚の見落としが数枚分に膨らむ構造です。

教訓としては、移行作業を「フォルダ内のファイルを変換する作業」だと思っていたのが間違いでした。記事の本文に書かれているURLまで含めて1セットです。定期的に本文をスキャンして非WebPを検出する仕組みにしておくべきでした。

テーマがやってくれること・やってくれないこと

ついでに、公開中のページのHTMLを実際に見て img タグの属性を確認しました。このブログは WordPress + SWELL です。

  • できていた
    全ての img に width と height が入っている。ブラウザがこの2つからアスペクト比を計算して領域を先に確保するので、画像読み込みによるレイアウトのガタつき(CLS)はこれで防げます
  • できていた
    srcset と sizes が自動生成され、画面幅に応じたサイズが選ばれる
  • できていた
    decoding="async" が付いている
  • 入っていなかった
    fetchpriority="high"。ファーストビューの画像に付けると読み込み開始が早まります

遅延読み込みは、src に1×1の透明GIFをdata URIで入れておき、あとで本物に差し替える方式でした。この方式は画面外の画像には効きますが、ファーストビューの画像に適用されると読み込みが1テンポ遅れてLCPが悪化します。幸いアイキャッチは対象外になっていましたが、「テンプレートの画像に一括で遅延読み込みを付ける」は事故になりやすいポイントです。

テーマやフレームワークが面倒を見てくれる範囲は思ったより広い一方、形式の選択と、ファーストビューの優先読み込みは自分で決める必要があるという切り分けでした。

まとめ|運用ルールとして残したこと

一度きりの変換作業では終わらないので、次のルールを手順書に書き込みました。

  • 上げる画像は全部 WebP
    アイキャッチも本文画像も例外なし
  • 差し替えは必ず新しいファイル名で
    同名上書きはCDNとSNSのキャッシュに勝てない
  • 公開したらSNSに投稿する前に og:image を確認
    投稿してからでは同じURLで直せない
  • 旧ファイルの削除は移行と分ける
    検索エンジンのインデックスが移るまで待つ
  • 手順書もセットで直す
    ファイルだけ移行して手順が古いままだと、必ず1枚だけ混ざる

数字としては75.69MBが6.61MBになったのがこの移行の成果ですが、実際にいちばん効いたのは「WebP以外を上げない」というルールを1つ決めたことでした。判断が要らなくなると、見落としが減ります。

見つかった18枚は、すべて新しいファイル名の WebP に差し替えました。結果は次のとおりです。

対象変換前変換後削減
本文画像 13枚1,070 KB517 KB51.7%
固定ページ 2枚327 KB158 KB51.7%
アイキャッチ 3枚303 KB152 KB49.8%
合計 18枚1,700 KB827 KB51.4%

ここで注目したいのは、削減率が91%ではなく51%にとどまっていることです。最初の移行はPNGからの変換だったので9割減りましたが、今回はすでに非可逆圧縮されたJPEGからの変換なので、削れる余地はその半分しかありません。

つまり最初からWebPで上げていれば得られたはずの差が、あとから変換しても取り戻せないということです。JPEGを一度経由した画像は、WebPにしても「JPEGの劣化を含んだWebP」にしかなりません。運用ルールを先に決めておく価値はここにあります。

変換後の画質は、before/afterのスクリーンショットのような文字が多い画像も含めて、原寸で見比べて劣化が分からないレベルでした。写真ベースの画像なら q84 で十分です。

最終的にサイトに残っている非WebPの画像は2種類だけになりました。

  • サイトアイコン(PNG)
    ブラウザやOSが読む用途なので、そのまま残す
  • プロフィールアイコン(JPEG・24KB)
    WebPに変換して測ったら23KBにしかならなかったので、据え置いた

最後の1枚は、ルールとしては変換すべきものです。ただ270×270の小さな画像で、変換しても1KBしか変わりません。効果を測ってから決めるという原則のほうを優先しました。全部そろえること自体が目的になると、意味のない作業が増えます。

なお、ここまでやって画像の転送量を91%削減したあと、実際にLighthouseで計測したらスコアは55点でした。画像はもう犯人ではなく、別のものが足を引っ張っていました。その調査は画像を91%減らしてもLighthouseは55点だったにまとめています。

次に読む記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

わどこんのアバター わどこん

実務12年のバックエンド・インフラエンジニア。バックエンド開発からクラウド・インフラの設計・構築・運用まで担当しています。主要言語は Java・Kotlin・PHP・Python。運用の現場で拾った知見を、再現できる手順に落として残すのがこのブログのテーマです。

目次