最先端なWebPメインなウェブサイトを作る with Nginx

WebP

ウェブサイトの転送量の低減化のためにも閲覧者の為にも写真や絵の画像は次世代フォーマットに対応させた方が良さげ。 で、次世代フォーマットは幾つかあるけど、現在はどれも「これだ」という状況ではない。おそらくGoogleが推すWebPが有力なんだろうなという程度。そのWebPはJPEG 2000推しのAppleがSafariブラウザでなかなか採用してくれないので「もうそろそろ世界中皆でWebPに切り替えた方が良くね?」という風に簡単にはいかない。世紀の糞ブラウザのIEももちろん非対応だし。

でも、「がとらぼ」の中の人的にはもうWebPに切り替えたくて我慢できなかった。やっぱりWebPファイルサイズの小ささを見ちゃうとね。何でSafariとIEの為に採用を待たなきゃならないのかという感じ。
と、いうことでWebP対応をしてみた。どうせWebPにするのなら「Jpeg/PNGメインだけどWebP対応ブラウザ対応なら特別にWebPで表示しちゃうよ」を飛び越えて「WebPメインだけどWebP非対応だったらお情けでJpeg/PNGで表示してあげる」という最先端なウェブサイトにしてみたい。今回はNginxを使って「がとらぼ」を最先端なWebPメインなウェブサイトにした方法のメモ。(Jpeg/PNGメインも一応途中に入れておく)

Jpeg/PNG形式の画像をWebP形式に変換

PNGやJpeg画像をWebPに変換するのはとても簡単。
FreeBSDならportsやpkgでgraphics/webpをインストールする。WebPへの変換用のコマンドはcwebpとなる。

いちばん手っ取り早く変換するならこれ。(普通はこれで十分)
% cwebp hoge.png -o hoge.webp
画質を指定するなら -q + 0〜100 をオプション指定する。0が最低画質。初期値は75
70〜80が実用的。
% cwebp -q 80 hoge.png -o hoge.webp
ファイルサイズが大して減少しなくてもどうしても画質を下げたくない(ロスレスにしたい)なら -lossless をオプション指定する。
% cwebp -lossless hoge.png -o hoge.webp

コマンドで画像を1つ1つ指定して変換するのは面倒で堪らないしウェブサイトによっては事実上不可能かもしれないのでスクリプトで纏めて変換するのが良い。

スクリプトのファイル名は任意。保存したら実行権限を与える (chmod +x ファイル名)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#! /usr/local/bin/bash

if [ $# -ne 1 ]; then
  echo "利用方法: $0 path" 1>&2
  echo "実行するには引数としてpathが必要です。" 1>&2
  exit 1
fi

find ${1}/ -type f -name "*.png" -exec cwebp -lossless {} -o {}.webp ;
find ${1}/ -type f -name "*.jpg" -exec cwebp {} -o {}.webp ;

コマンドには引数として変換したいファイルのあるpathを指定する。指定したpathの下層ディレクトリは再帰的に辿って変換する。
面白いことに(特に図などの)PNGを変換する場合に、cwebpの自動まかせではなくロスレス指定した方がボケない上にサイズが小さくなることが多い。なので上のサンプルでは-losslessオプションを付けている。これが余計だったら9行目の -lossless を取って使ってください。

% スクリプト名 /dir/path

この場合はhoge.pngはhoge.png.webp、hage.jpgはhage.jpg.webpになる。この形式の名前は後で使う。元のJpeg/PNGファイルはそのまま残る。

関連サイト: WebP変換 (悪いインターネット) コマンドじゃなくてインターネットのオンラインツールで変換。

手動でコンテンツ書き換え

WebPを表示するにあたり真面目にコツコツとウェブサイトのコンテンツを変更するつもりなら以下。

<img src="/path/hoge.png" alt="Hoge" />
もともとこういうタグで書いていたのなら
<picture>
    <source type="image/webp" srcset="/path/hoge.webp" />
    <img src="/path/hoge.png" alt="Hoge" />
</picture>

こんな感じで書く。

しかし、これは対応が面倒だし、将来的に全てのブラウザがWebP対応になったら再び書き換えることになると思われる。それも大変。

Jpeg/PNGなコンテンツのまま「特別」にWebP画像を出力

ウェブのコンテンツの側は一切書き換えずに、Jpeg/PNGアクセスがあったらウェブサーバの側でWebP対応のブラウザに「特別に」WebP画像ファイルを渡すという方法もある。 ウェブサーバがNginxの場合はとても簡単。

Nginxの設定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name  www.example.com;

中略

    location ~* ^.+.(jpe?g|png) {
        add_header Vary "Accept-Encoding";
        try_files $uri$webp_suffix $uri =404;
        expires max;
    }
}

ググったら判をついたようにこればっかり出てくる。誰が何処に書いたのがオリジナルなのか判らないくらい。
mapの側で$http_acceptを見て、webp対応のエージェントなら変数$webp_suffixに".webp"をセット。
locationの側で、リクエストされたURIがJpegやPNGのものであればそのファイル名に.webpを付けたファイル名(のファイル)を送信する。リクエストと違うファイルを返すのでVaryヘッダを付ける。ブラウザの側に保存期間をmaxで指定する。expiresは別で指定してるよという場合もここで再指定。

これで、WebP対応のブラウザで hoge.jpgやhoge.pngにアクセスがあったらhoge.jpg.webpやhoge.png.webpを送信してくれる。

ところで、これはコンテンツを書き換えないということで <img src="/path/hoge.jpg" alt="Hoge" />のままになる。
せっかくWebP対応を行うならコンテンツの画像ファイルの名前をWebpにしたい。かといってpictureタグに書き換えるのもイヤ。
つまり、<img src="/path/hoge.webp" alt="Hoge" /> こうしたい。

WebPメインなコンテンツで「お情けで」Jpeg/PNGを出力

このために必要な処理は、「WebP対応のブラウザでWebPファイルへのアクセスがあったら当然WebP画像ファイルを送信するが、WebP非対応のブラウザでWebPファイルへのアクセスがあったらJpegを送信しようとする。Jpegが無ければPNGを送信する。」というもの。

コンテンツ側の対応。PNGやJpeg画像のURLの拡張子をwebpに書き換える。
今度はhoge.jpg.webpとかhoge.png.webpではなく、hoge.jpgでもhoge.pngでもhoge.webpとする。
WordPressだったらSearch Regexプラグインを使う。

関連記事: WordPressの一括変換用Search Regexプラグイン

画像ファイル側の対応

先程WebP用の画像のファイル名をhoge.jpg.webpやhoge.png.webpにした。なのに今度はhoge.webp。スンマセン

お詫びに再帰的にファイル名を書き換えるスクリプト。
スクリプトのファイル名は任意。保存したら実行権限を与える (chmod +x ファイル名)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#! /usr/local/bin/bash

#引数にpathを指定して実行のこと。
if [ $# -ne 1 ]; then
  echo "利用方法: $0 path" 1>&2
  echo "実行するには引数としてpathが必要です。" 1>&2
  exit 1
fi

find ${1}/ -name "*.png.webp" -type f -print0 | xargs -0 -I {} sh -c 'mv "{}" "$(dirname "{}")/`echo $(basename "{}") | sed 's/.png.webp$/.webp/g'`"'
find ${1}/ -name "*.jpg.webp" -type f -print0 | xargs -0 -I {} sh -c 'mv "{}" "$(dirname "{}")/`echo $(basename "{}") | sed 's/.jpg.webp$/.webp/g'`"'

これでhoge.jpg.webpやhoge.png.webpをhoge.webpにリネームする。
注意: xBSDとLinuxなどの両方でいけるように作ったつもりだけどFreeBSDで使っただけなのでLinuxでこのまま動くかは未確認。

Nginx側の設定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
map $http_accept $webp {
    default   "";
    "~*webp"  $uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name  www.example.com;

中略

    location ~ (.+).webp$ {
        set $wo_ext $1;
        set $jpg "${wo_ext}.jpg";
        set $png "${wo_ext}.png";
        add_header Vary "Accept-Encoding";
        try_files $webp $jpg $png $uri =404;
        expires max;
    }
}

mapの方は先の場合とほぼ一緒。変数名を$webp_suffixではなく$webpにしたのと、セットする内容がリクエストのあったURI(のパラメータ無し)というところが違う。これで、WebP対応ブラウザでアクセスがあれば$webp変数にURIが入る。WebP非対応ブラウザでアクセスがあれば$webp変数は空がセットされる。
locationの側はWebP画像へのリクエストがあったらURIの拡張子を除いた部分を$wo_ext変数にセット。
$jpg変数に$wo_ext + ".jpg"をセット。これでJpeg用のURIが出来る。
$png変数に$wo_ext + ".png"をセット。これでPNG用のURIが出来る。
if文を使ってよければwebp対応ブラウザの場合はVaryヘッダを付けないという書き方もできるが、Nginxではif文は忌み嫌われていて余程のことでなければ使わないということになっているようなので、それに倣ってif文は使わず、WebP対応時にもVaryヘッダを付ける。実害は無い筈。
try_filesで$webp $jpg $pngを並べた。WebP対応ブラウザであれば$webp変数にWebP画像のURIがセットされていてそれが先頭に指定されているのでそれが表示されることになる。(もしもWebPが無ければJpegが、それもなければPNGが表示される)
WebP非対応ブラウザでは$webp変数は空なので実質的にtry_filesは$jpgと$pngが並んだ状態となる。先に$jpgで挑戦してJpegファイルが存在するならそれが表示される。Jpegが存在しないならPNGファイルが表示される。それも無ければError 404になるが、それは元々表示できる画像が無いということ。
このやり方と設定はググっても無かったので「がとらぼ」の中の人が無い知恵を絞って作った一応オリジナル。これで「がとらぼ」は表示できているようなので問題無しと判断している。WebP非対応ブラウザはIEで確認。何かとんでもなく間違っていたらご指摘いただければと。

問題はAMP。上でやったことにより、実質的にWebP専用コンテンツになっている。救済としてWebP非対応だったらJpegやPNGが表示できる状態だが、その処理が入っているのは自前のウェブサーバだけ。AMPはGoogleのAMPキャッシュであればGoogleのサーバなのでこの処理は通用しない。つまりAMPのコンテンツに書かれた画像のURIがWebPで、Googleのクローラーが拾っていった画像がWebPということになるとそのAMPキャッシュはiPhoneのSafariでアクセスしたらどうなるのだろう。Googleが気を利かせてSafariには自動的にJpegやPNGで出力してくれるならそれでOKだが、そうでなければSafariには画像は表示されないことになる。iPhoneは個人的には持ってないし周りの人も偶然にも誰も持っていないので確認のしようがない。
個人的にはもうAppleの端末はどうでもいいと思い始めてるけど、iPhoneの日本のモバイルでの比率を考えたら普通は無視できないよね。