WebPだから軽い、と思っていた——56pxの枠に512pxを79枚並べて図鑑が壊れた話
収集RPG「四十秒のオリ図鑑」で、図鑑のオリが1体だけ絵の出ない状態になりました。名前とレベルは出ているのに、絵の場所だけが空。ビンヅメというオリでした。
まず画像を疑って、外れた
順当に、そのファイルが壊れたと思いました。ローカルで開くと正常。本番のURLを叩いても200で返ってきて、サイズもローカルと1バイト違わない。画像は無実です。
次に、再現の条件を思い出しました。空になったのは、別のゲーム(フルスタック!というカードRPG)をひと通り遊んだ直後に図鑑を開いたときでした。1回だけの偶然に見えたので、しばらく放っておきたい気持ちもあったのですが、iPhoneで遊んでいて2回目が出たのでちゃんと数えることにしました。
数えたら79MBだった
図鑑はオリを一覧で並べる画面です。当時の実装は、セルごとにこう書いていました。
<img src="img/binzume.webp">
.dex-cell img { width: 56px; height: 56px; }
1枚52KBのWebPです。79枚あっても4MB。転送量だけ見ればなんてことはありません。私はここで安心していました。
安心していい話ではありませんでした。ブラウザが画面に絵を出すには、圧縮を解いて生のピクセルに戻す必要があります。512×512を4バイト(RGBA)で持つと1枚あたり約1MB。79枚だと79MB。実際に表示されるのは56pxの枠なので、そのうち99%近くは、置いた瞬間に捨てられる情報です。
iOSのSafari、それもアプリ内ブラウザは、この手のメモリが厳しくなると画像のデコードを静かにあきらめます。エラーは出ません。読み込みイベントも来ないまま、そこだけ空になる。重いゲームを遊んだ直後という条件は、まさに残りメモリが少ない瞬間でした。
直しかたは地味です
図鑑用に128pxのサムネイルを別に作って、一覧はそちらを読むようにしました。ImageMagickで一括です。
magick binzume.webp -resize 128x128 thumb/binzume.webp
79枚で688KB。デコード後は4.9MBまで落ちました。79MBから16分の1です。詳細画面と戦闘画面は今までどおり512pxを使っています。大きく見せる場所では必要な解像度なので、そこは変えていません。
ついでに一覧の img に loading="lazy" と decoding="async" を付けました。画面外のぶんは、そもそも読まないほうが安いです。
「軽い」を何で測っていたか
WebPにしてファイルサイズを見て、軽いと判断していました。転送は軽かったんです。ただ、画面に並べた瞬間に効いてくるのは解像度×枚数のほうでした。同じ絵でも、1枚を大きく出すのと、79枚を小さく並べるのとでは必要なものが違う。素材を1種類しか持っていなかったのが原因で、WebPは何も悪くありませんでした。
白状すると、画像を用意するとき「あとで縮小できるから大きめで」と考えるのが癖になっていました。縮小はできます。でも表示時に縮小するのは、ブラウザに毎回大きい絵を開かせることでもあります。用途ごとにサイズ違いを持つ、というのは昔からの当たり前の話で、それを自分の手が忘れていただけでした。
気づけなかった理由
この不具合、開発中は一度も出ませんでした。パソコンのブラウザはメモリに余裕があるので、79MBくらい平気で持ちます。実機でも、図鑑を単体で開いているぶんには落ちません。他の重いページを経由したあとにだけ壊れる。テストの手順に「別のゲームを遊んでから開く」なんて書いてありません。
なので、レビューの観点にこう足しました。画像を一覧で並べる画面は、実際の表示サイズと元画像の解像度の差を確認する。差が4倍を超えていたら、サムネイルを別に持つ。数えるだけなので、次はもっと早く気づけるはずです!