卒業研究の中間発表用に管理画面の操作を4.5秒だけ録画してGIFにしたところ、1.5MBありました。解像度もフレームレートも下げて一晩かけて、3割ほどしか減りません。原因は設定ではなくGIFという形式そのものでした。その確かめ方を、実際に踏んだ3つの手順としてまとめます。
公開できる録画がなかったので、PythonのPillowで架空の管理画面アニメーションを1フレームずつ描きました。960×600、75フレーム、各フレーム60ms、合計4.5秒、表が一行ずつ埋まりポインタがフィルタへ移動して確認ダイアログが出る、という内容です。実際の画面収録に近づけるため2倍サイズで描いてから縮小しています。省くとフレームが単純な色面だけになり、GIFが不自然に小さくなるためです。1,585,112バイト。自作の架空サンプルで、実在する製品のスクリーンショットではありません。
描いている途中で最初の発見がありました。各フレームの表示時間を66msに指定したのに、読み直すと60msになっていたのです。GIFのフレーム遅延は100分の1秒単位で保存されるため、66msはそもそも表現できません。フレーム間隔は必ず10msの倍数になります。
手順1:中身の数を数える
「フレーム」はアニメーションを構成する1枚の静止画、「カラーテーブル(パレット)」はそのフレームが使ってよい色の一覧で、各ピクセルは色そのものではなく一覧の何番かを持っています。GIFはこの一覧を最大256色に制限し、しかもテーブルはファイル全体で共有されるとは限らずフレームごとに持てます。今回のサンプルは75フレーム中74フレームが各自256色のテーブルを抱えていました。1色3バイトなので、絵の中身を数え始める前に約57KBです。
もう一つ数えたのが「ダーティ矩形」です。GIFのフレーム間最適化は部分更新、そのフレームでは矩形ひとつぶんだけ描き直し、外側は前のフレームを引き継ぎます。ただし矩形は1フレームに1つだけ。今回の録画はポインタが左、ヘッダの時計が右、検索欄のカーソルが中央と離れているため、3か所を囲む矩形が画面の大半に広がります。実測では75フレーム中68フレームが部分更新を使っていたのに、平均のダーティ矩形は画面の59%を占めていました。全画面グラデーションの別サンプルでは、30フレーム中1フレームも部分更新できず100%です。
手順2:同じ形式でもう一度圧縮してみる
縮尺70%、2フレームに1枚、64色まで減色という一般的な手当てを済ませた357,648バイトのGIFを、もう一度GIFとして書き出しました。結果は356,964バイト。684バイトしか減らず、フレームを1枚ずつ比べるとピクセルは完全に同一でした。ツールの性能ではなく、同じ形式の中に減らせる余地が残っていなかったわけです。この手順を挟めば「まだ圧縮できるのか」を数字で判定できます。減らないなら次は形式を変える段階です。
手順3:形式を変えて比べる
同じ録画で、GIFが983,087バイト、アニメーションWebPが1,045,924バイト、APNGが1,392,726バイト、アニメーションAVIFが157,490バイトでした。APNGは可逆でピクセルが元と完全一致するため、減らないのは当然です。意外だったのはWebPで、この録画では34%しか減らず、GIF再圧縮とほとんど変わりません。ところが先ほどのグラデーションのサンプルでは、GIFが14%減なのにWebPは76%減ります。同じエンコーダで差がここまで開く理由は、まだ分かっていません。
手順2で頭打ちだった357,648バイトのファイルも、AVIFなら62%減ります。「最適化済みのGIFはもう縮まない」は同じ形式の中でだけ成り立つ話でした。
ただしAVIFを常用してくださいとは言えません。デコードと再生を確認したのはデスクトップのブラウザエンジン3種だけで、メッセージアプリ、古い端末、アプリ内蔵のWebViewは一つも試していません。結果カードにも、このアニメーションAVIFは透過を含まないと書かれています。私はいま、相手の再生環境が分からないときはWebP、自分で制御できるときはAVIF、どうしてもGIFのときはフレーム間引き、という使い分けです。間引きは28.8%減で残ったフレームの画質は落ちませんが、総再生時間が4.5秒から2.28秒に縮む点だけ忘れないようにしています。

