ウェブページの表示速度はとても重要です。ページの表示が遅ければ遅いほどサイトの閲覧を諦めてしまう訪問者は多くなります。
HTML や画像などのコンテンツを GZIP や Brotli で圧縮すると、 ネットワーク転送時間が短くなり、 より早くウェブページが表示されるようになります。
今回は、 Apache で稼働している静的サイトを事前圧縮コンテンツを用意することで高速化する方法を紹介します。この方法は、 近年、 人気が高まっている静的サイトジェネレーターとの相性も良く、 レンタルサーバーを間借りしている環境でも適用しやすいです。サーバーの管理権限がないユーザーでも一般的な権限があれば高速化を実現できます。
オンデマンド圧縮 vs 事前圧縮
Apache で圧縮されたコンテンツを返す方法は大きく分けて 2 種類あります。オンデマンド圧縮と事前圧縮です。
オンデマンド圧縮
オンデマンド圧縮はブラウザーからのリクエスト時にコンテンツをリアルタイムで圧縮して返す方式です。オリジナルのコンテンツさえ置いておけば、 Apache がコンテンツを圧縮して返してくれます。コンテンツの管理が煩雑になりません。
- htdocs
- index.html
この方式はリアルタイムで圧縮をおこなうため、 CPU 負荷が高くなる (サーバー全体でのスループット低下要因になる)、 サーバーの処理時間が少し長くなるというデメリットがあります。サーバーの処理時間が長くなっても、 それ以上にネットワーク転送の時間が短縮できればトータルでは時間短縮になります。
Apache では古くから mod_deflate
によって gzip 圧縮と deflate(zlib)圧縮がサポートされています。さらに、 Apache 2.4.26 以降では mod_brotli
が追加され、 Google が開発した脅威の圧縮アルゴリズム Brotli (ブロートリ) も利用可能になりました。
これらの圧縮機能を有効にするためには httpd.conf
でモジュールを有効化する必要があります。Apache の管理者であれば簡単なことですが、 レンタルサーバーで Apache を間借りしているような場合は mod_deflate
、 mod_brotli
が有効化されておらず利用できないケースもあります。
とくに、 mod_brotli
は新たに追加されたモジュールであるため、 まだまだ利用できない環境のほうが多いのではないかと思います。
それに、 せっかくサイトを高速化しようというのに、 圧縮処理で少し時間をロスしてしまうのももったいないですよね。
事前圧縮
コンテンツを事前に圧縮しておけばいいんじゃないの?
その通りですね。ブラウザーのリクエストに対してあらかじめ圧縮しておいたコンテンツを返すように Apache を構成することができます。ブラウザーが index.html
を要求したときに、 圧縮済みの index.html.gz
や index.html.brotli
を返すようにするわけです。そのためには、 オリジナルのコンテンツだけでなく圧縮済みのファイルも一緒に配置しておく必要があります。(圧縮ファイルを自動的に出力してくれる CMS も登場しています。)
- htdocs
- index.html
- index.html.brotli
- index.html.gz
この方式はファイル数が増えるため管理が少し面倒でサーバーのディスクスペースも多く消費します。ですが、 事前圧縮方式には、 圧縮処理の時間ロスがない、 古い Apache でも最新の Brotli 圧縮に対応できる、 レンタルサーバーでも利用できる環境が多い、 といった大きなメリットがあります。
圧縮ツール
GZIP 圧縮ファイル、 Brotli 圧縮ファイルを作成するツールを紹介します。
Zopfli
Google の開発した Zopfli は、 圧縮率の高い GZIP 圧縮ファイルを作成できるツールです。Zopfli の公式サイトではビルド済みのバイナリは提供されていません。Windows 用のバイナリを garyzyg さんが配布してくれています。Windows ユーザーの方はこちらを使わせていただきましょう。
zopfli.exe -h
でヘルプが表示されます。
コマンドプロンプトC:¥>zopfli.exe -h Usage: zopfli [OPTION]... FILE... -h gives this help -c write the result on standard output, instead of disk filename + '.gz' -v verbose mode --i# perform # iterations (default 15). More gives more compression but is slower. Examples: --i10, --i50, --i1000 --gzip output to gzip format (default) --zlib output to zlib format instead of gzip --deflate output to deflate format instead of gzip --splitlast ignored, left for backwards compatibility
sample.html
を GZIP 圧縮して sample.html.gz
を作成する場合は以下のようにします。
コマンドプロンプトC:¥>zopfli.exe sample.html
Brotli
Brotli も Google が開発した圧縮アルゴリズムです。Deflate 互換ではありませんが、 GZIP を凌ぐ高い圧縮率を実現しています。Windows 用バイナリは Brotli 公式サイトからダウンロードできます。
バージョンによって、 ソースコードのみの配布だったり、 バイナリの配布もあったりと気まぐれなようです。私が確認したときは、 v1.0.7、 v1.0.6、 v1.0.5 はソースコードのみの配布、 v1.0.4 まで遡ると Windows 用のバイナリも配布されていました。
brotli.exe -h
でヘルプが表示されます。
コマンドプロンプトC:¥>brotli.exe -h Usage: brotli.exe [OPTION]... [FILE]... Options: -# compression level (0-9) -c, --stdout write on standard output -d, --decompress decompress -f, --force force output file overwrite -h, --help display this help and exit -j, --rm remove source file(s) -k, --keep keep source file(s) (default) -n, --no-copy-stat do not copy source file(s) attributes -o FILE, --output=FILE output file (only if 1 input file) -q NUM, --quality=NUM compression level (0-11) -t, --test test compressed file integrity -v, --verbose verbose mode -w NUM, --lgwin=NUM set LZ77 window size (0, 10-24) window size = 2**NUM - 16 0 lets compressor choose the optimal value -S SUF, --suffix=SUF output file suffix (default:'.br') -V, --version display version and exit -Z, --best use best compression level (11) (default) Simple options could be coalesced, i.e. '-9kf' is equivalent to '-9 -k -f'. With no FILE, or when FILE is -, read standard input. All arguments after '--' are treated as files.
sample.html
を Brotli 圧縮して sample.html.brotli
を作成する場合は以下のようにします。
コマンドプロンプトC:¥>brotli.exe -o sample.html.brotli sample.html
オプション -q
で圧縮レベルを指定できますが、 既定値が 11
(最高圧縮) なので省略して構わないでしょう。
オプション -o
で出力ファイル名を明示的に指定しています。このオプションを省略した場合は、 拡張子 .br
が付加された sample.html.br
が作成されます。Apache では拡張子 .br
を扱いづらいので、 .brotli
にしておくのがオススメです。詳細は後述します。
mod_rewrite
mod_rewrite
はリクエストやレスポンスを条件に合わせて書き換え (rewrite) できる便利な Apache モジュールです。
このモジュールを使えば、 拡張子が .html
、 .css
、 .js
のファイルが要求されて、 かつ、 要求されたファイルの末尾に .gz
を付加したファイルが存在するなら、 そのファイルを代わりに返す、 といったことが実現できます。
レスポンスヘッダーの変更も必要になるので mod_headers
モジュールも有効になっている必要があります。
httpd.confLoadModule rewrite_module modules/mod_rewrite.so
LoadModule headers_module modules/mod_headers.so
.htaccess
で圧縮を有効化する場合は、 AllowOverride
に FileInfo
(または All
) が含まれている必要があります。AllowOverride
が None
になっていたり FileInfo
が含まれていない Apache 環境では、 .htaccess
で圧縮を有効化することができません。
httpd.confAllowOverride FileInfo
上記の設定がなされている Apache 環境であればレンタルサーバーを間借りしている人でも、 .htaccess
で圧縮コンテンツを返すように構成することができます。
.htaccess
事前圧縮コンテンツを返すように構成した .htaccess
は以下の通りです。このファイルをサイトのディレクトリに配置します。圧縮を有効にしたい一番上のディレクトリに .htaccess
を 1 つ置けば、 下位ディレクトリにも設定が作用します。
.htaccess<IfModule rewrite_module>
<IfModule headers_module>
RewriteEngine on
#
# Brotli
#
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_URI} ¥.(html|css|js)$
RewriteCond %{REQUEST_FILENAME}¥.brotli -s
RewriteRule .* %{REQUEST_URI}.brotli [L]
<Files *.html.brotli>
Header set Content-Encoding br
ForceType text/html
</Files>
<Files *.css.brotli>
Header set Content-Encoding br
ForceType text/css
</Files>
<Files *.js.brotli>
Header set Content-Encoding br
ForceType application/javascript
</Files>
#
# GZIP
#
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_URI} ¥.(html|js|css)$
RewriteCond %{REQUEST_FILENAME}¥.gz -s
RewriteRule .* %{REQUEST_URI}.gz [L]
<Files *.html.gz>
Header set Content-Encoding gzip
ForceType text/html
</Files>
<Files *.css.gz>
Header set Content-Encoding gzip
ForceType text/css
</Files>
<Files *.js.gz>
Header set Content-Encoding gzip
ForceType application/javascript
</Files>
#
# Vary
#
<FilesMatch "¥.(html|css|js)(¥.gz|¥.brotli)?$">
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
</IfModule>
順番に説明していきましょう。
IfModule
IfModule
はディレクティブは、 指定したモジュールがロードされている場合のみ、 中に書かれているディレクティブ (指令) を有効にします。
<IfModule rewrite_module>
# rewrite_module がロードされている場合のみ、
# この範囲に記述されたディレクティブ(指令)が有効になります。
</IfModule>
.htaccess
では忘れずに IfModule
を書くようにしましょう。
httpd.conf
の場合は IfModule
を書かかなくてもモジュールのロード不足があれば Apache 自体が起動しなくなるので、 必ず、 設定ミスに気付くことができます。
しかし、 .htaccess
の場合は違います。.htaccess
は Apache の起動後、 リクエストがあったときに評価されます。そして、 ロードされていないモジュールのディレクティブを使っているといった設定ミスがあるとブラウザーには 500 Internal Server Error が返されてしまいます。そうならないように、 .htaccess
ではディレクティブが使用できるかどうかを IfModule
でチェックすることが重要です。
きちんと IfModule
が書かれていれば、 rewrite_module
や headers_module
が使えないサーバー環境でもエラーが発生することなく、 (従来通り) 圧縮されていないオリジナルのコンテンツがブラウザーに返されます。
RewriteEngine on
RewriteEngine on
はリライトエンジンを有効にします。これによって、 後続の RewriteCond
や RewriteRule
が有効になります。
RewriteEngine on
GZIP
Brotli よりも先に GZIP の説明をしましょう。
#
# GZIP
#
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_URI} ¥.(html|js|css)$
RewriteCond %{REQUEST_FILENAME}¥.gz -s
RewriteRule .* %{REQUEST_URI}.gz [L]
複数の RewriteCond
ディレクティブが続いて、 その後に RewriteRule
ディレクティブがあります。RewriteCond
に書かれた条件をすべて満たすと、 RewriteRule
に書かれたルールが適用されます。
RewriteCond %{HTTP:Accept-Encoding} gzip
これは、 Accept-Encoding
ヘッダーの値に gzip
が含まれている場合に成立する条件です。
RewriteCond %{REQUEST_URI} ¥.(html|css|js)$
これは、 要求された URI の末尾が .html
、 .css
、 .js
のいずれかである場合に成立する条件です。
RewriteCond %{REQUEST_FILENAME}¥.gz -s
これは、 要求されたファイルの末尾に .gz
を付加したファイルが存在してファイルサイズが 0 よりも大きい場合に成立する条件です。
RewriteRule .* %{REQUEST_URI}.gz [L]
これは上記 3 つの条件を満たした場合に適用される書き換えルールです。上記の場合、 元の URL に .gz
が付加された URL に書き換えられます。
以上で、 ブラウザーが Accept-Encoding
ヘッダーに gzip
を付けて HTML/CSS/JS をリクエストして、 サーバーに対応する .gz
ファイルが置いてあれば、 そのファイルを返すという動作になります。
ただ、 ブラウザーに返すファイルを差し替えるだけでは上手くいきません。ブラウザーはサーバーが返したデータが何なのか認識できず文字化けしたページを表示してしまいます。
コンテンツが GZIP 圧縮されていること、 中身のコンテンツが HTML であることをブラウザーに伝えるためにレスポンスにヘッダーを追加する必要があります。
<Files *.html.gz>
Header set Content-Encoding gzip
ForceType text/html
</Files>
この Files
ディレクティブがその指定です。ファイル名が *.html.gz
に合致する場合、 中に書かれたディレクティブが実行されます。
Header set Content-Encoding gzip
によって、 Content-Encoding: gzip
というヘッダーがレスポンスに追加されます。これによって、 ブラウザーはコンテンツが GZIP 圧縮されていることを認識できるようになります。
ForceType text/html
によって、 Content-Type: text/html
というヘッダーがレスポンスに追加されます。これによって、 ブラウザーは圧縮データを展開した後の中身が HTML であることを認識できるようになります。
Apache には mime.types
というファイルがあり、 拡張子ごとの MIME Type (Content-Type) が定義されています。ファイルの拡張子が .html
の場合は自動的に Content-Type: text/html
が追加されるのですが、 今は rewrite_module によって URL 末尾の .html
を .html.gz
に書き換えてしまっています。そのため、 Apache は拡張子 .gz
に対応した MIME Type を Content-Type
ヘッダーに設定しようとします。
これでは都合が悪いので、 ForceType
で強制的に text/html
を指定しているわけです。
残りの .css
と .js
も同様です。Content-Encoding
ヘッダーと Content-Type
ヘッダーが適切に設定されるようにしています。
<Files *.css.gz>
Header set Content-Encoding gzip
ForceType text/css
</Files>
<Files *.js.gz>
Header set Content-Encoding gzip
ForceType application/javascript
</Files>
Brotli
Brotli 圧縮の設定も GZIP 圧縮と同様です。
#
# Brotli
#
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_URI} ¥.(html|css|js)$
RewriteCond %{REQUEST_FILENAME}¥.brotli -s
RewriteRule .* %{REQUEST_URI}.brotli [L]
<Files *.html.brotli>
Header set Content-Encoding br
ForceType text/html
</Files>
<Files *.css.brotli>
Header set Content-Encoding br
ForceType text/css
</Files>
<Files *.js.brotli>
Header set Content-Encoding br
ForceType application/javascript
</Files>
Brotli に対応しているブラウザーは Accept-Encoding
ヘッダーに br
を含めてリクエストを送信してきます。これを条件として Brotli 圧縮したファイルを返すように設定しています。
次の 2 点に注意してください。
Brotliの書き換えルールはGZIPの書き換えルールよりも前に書く
Brotli に対応しているブラウザーは GZIP にも対応しているので、 以下のようなヘッダーを送ってきます。
Accept-Encoding: gzip, deflate, br
GZIP の書き換えルールが先に書かれていると、 Brotli に対応しているブラウザーに対しても GZIP 圧縮コンテンツを優先して返してしまうことになります。これでは Brotli 圧縮したファイルを配置している意味がなくなってしまいます。
Brotli の書き換えルールは GZIP の書き換えルールよりも必ず前に書きましょう。
Brotli圧縮したファイルの拡張子は .br ではなく .brotli にしよう
Brotli に対応していることを示すエンコーディング名は br
です。Accept-Encoding
ヘッダーにも brotli
ではなく br
という文字列が設定されます。
そして、 Brotli 圧縮されたファイルの拡張子にも一般的に .br
が使われます。しかし、 .br
という拡張子は Apache との相性が非常に悪いのです。br
はブラジルの国コードと同じだからです。Apache にはファイルの拡張子によってコンテンツの言語を自動的に識別する仕組みがあります。index.html
の日本語版は index.html.ja
、 英語版は index.html.en
といった具合です。そうなると、 ブラジル語版は index.html.br
ということになりますよね。
つまり、 Brotli 圧縮したファイルのつもりでサーバーに index.html.br
を配置すると、 Apache はブラジル語のコンテンツだと誤って認識してしまうのです。その結果、 Apache はレスポンスに Content-Language: br
(このコンテンツはブラジル語です) を追加します。
強制的に Content-Language: ja
で上書きすることもできますが、 固定値 ja
で上書きしてしまうのもイマイチです。これでは、 せっかくの言語別にコンテンツを持てるという Apache の機能が活かせなくなってしまいますから。
「国コード br
と区別できなくなるので Brotli の拡張子に br
を使わない」 これが最良の解決方法です。
Vary
最後は Vary
の説明です。Vary
ヘッダーはキャッシュサーバー向けの指示です。オリジンサーバーは、 リクエストヘッダーの内容によってレスポンスを変えたことを Vary
ヘッダーを使ってキャッシュサーバーに伝えることができます。
Varyがなかったら?
Vary
ヘッダーがなかったら、 どのような問題が起こるのかを考えてみましょう。
キャッシュサーバーはクライアントからのリクエストに対して (オリジンサーバーにリクエストを転送することなく) キャッシュしているコンテンツを返します。
Chrome を使っているクライアント A と Internet Exproler を使っているクライアント B があるとします。
はじめに、 クライアント A (Chrome) が /sample.html
をリクエストします。Chrome は Brotli に対応しているので Accept-Encoding
ヘッダーは以下のようになります。
Accept-Encoding: gzip, deflate, br
キャッシュサーバーはこのリクエストをオリジンサーバーに転送します。そうすると、 オリジンサーバーは Brotli 圧縮されたレスポンスを返します。
Content-Encoding: br
Content-Type: text/html
キャッシュサーバーはオリジンサーバーのレスポンスをクライアント A に返します。このとき、 /sample.html
に対応するレスポンスとしてキャッシュしておきます。
次に、 クライアント B (Internet Explorer) が /sample.html
をリクエストします。Internet Explorer は Brotli に対応していないので Accept-Encoding
ヘッダーは以下のようになります。
Accept-Encoding: gzip, deflate
キャッシュサーバーは /sample.html
に対応するキャッシュをすでに持っているので、 オリジンサーバーにリクエストを転送することなく、 キャッシュの内容をクライアント B に返します。
Content-Encoding: br
Content-Type: text/html
しかし、 キャッシュされているこのレスポンスは Brotli 圧縮されたものです。クライアント B (Internet Explorer) は、 このレスポンスを処理することができません。
これでは困りますよね。Vary
ヘッダーはこのような問題を解決します。Vary
ヘッダーによってキャッシュサーバーの振る舞いがどのように変わるのかを見てみましょう。
Varyがあれば
はじめに、 クライアント A (Chrome) が /sample.html
をリクエストします。
Accept-Encoding: gzip, deflate, br
キャッシュサーバーはこのリクエストをオリジンサーバーに転送します。そうすると、 オリジンサーバーは Brotli 圧縮されたレスポンスを返します。
Content-Encoding: br
Content-Type: text/html
Vary: Accept-Encoding
キャッシュサーバーはオリジンサーバーのレスポンスをクライアント A に返します。そして、 レスポンスをキャッシュするのですが、 このときに Vary
ヘッダーを考慮します。
Vary
ヘッダーの値は Accept-Encoding
になっています。これは 「リクエストの Accept-Encoding
ヘッダーの内容によってレスポンスが変わりましたよ」 という意味です。
クライアント A (Chrome) がリクエストで送信した Accept-Encoding
ヘッダーは以下の内容でした。
Accept-Encoding: gzip, deflate, br
キャッシュサーバーは 「/sample.html
をリクエストしたときのレスポンス」 としてキャッシュするのではなく、 「Accept-Encoding: gzip, deflate, br
ヘッダー付きで /sample.html
をリクエストしたときのレスポンス」 としてキャッシュします。
次に、 クライアント B (Internet Explorer) が /sample.html
をリクエストします。Internet Explorer は Brotli に対応していないので Accept-Encoding
ヘッダーは以下のようになります。
Accept-Encoding: gzip, deflate
キャッシュサーバーは 「Accept-Encoding: gzip, deflate, br
ヘッダー付きで /sample.html
をリクエストしたときのレスポンス」 をキャッシュしていますが、 これは Accept-Encoding: gzip, deflate
ヘッダーを付けてきたクライアント B (Internet Explorer) に返してよいレスポンスではありません。
キャッシュがヒットしなかったので、 キャッシュサーバーはクライアント B からのリクエストをオリジンサーバーに転送します。そうすると、 オリジンサーバーは GZIP 圧縮されたレスポンスを返します。
Content-Encoding: gzip
Content-Type: text/html
Vary: Accept-Encoding
キャッシュサーバーはオリジンサーバーのレスポンスをクライアント B に返します。そして、 このレスポンスも Vary
ヘッダーを考慮して 「Accept-Encoding: gzip, deflate
ヘッダー付きで /sample.html
をリクエストしたときのレスポンス」 としてキャッシュします。
結果として、 キャッシュサーバーは以下の 2 つのレスポンスを区別してキャッシュしている状態になります。
Accept-Encoding: gzip, deflate, br
ヘッダー付きで/sample.html
をリクエストしたときのレスポンスAccept-Encoding: gzip, deflate
ヘッダー付きで/sample.html
をリクエストしたときのレスポンス
これならキャッシュサーバーは、 クライアントの Accept-Encoding
ヘッダーに応じて適切なレスポンスを返すことができますね。
Vary
ヘッダーの役割が分かったので、 .htaccess
の説明に戻りましょう。以下の設定で Vary
ヘッダーを付加するように指定しています。
#
# Vary
#
<FilesMatch "¥.(html|css|js)(¥.gz|¥.brotli)?$">
Header append Vary Accept-Encoding
</FilesMatch>
FilesMatch
ディレクティブは Files
ディレクティブと似ています。FilesMatch
ディレクティブでは正規表現を使って合致条件を書くことができます。
正規表現 \.(html|css|js)(\.gz|\.brotli)?$
は以下の拡張子を持つファイルにマッチします。
.html
.html.gz
.html.brotli
.css
.css.gz
.css.brotli
.js
.js.gz
.js.brotli
これらのファイルにマッチした場合、 Vary: Accept-Encoding
ヘッダーが付加されます。
事前圧縮したコンテンツで Apache 静的サイトを高速化する手順は以上です。