Apache静的サイト高速化! mod_rewriteで圧縮コンテンツを返す
2019-06-27 Web

ウェブページの表示速度はとても重要です。ページの表示が遅ければ遅いほどサイトの閲覧を諦めてしまう訪問者は多くなります。

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.conf
LoadModule rewrite_module modules/mod_rewrite.so LoadModule headers_module modules/mod_headers.so

.htaccess で圧縮を有効化する場合は AllowOverride FileInfo または All が含まれている必要があります。AllowOverride None になっていたり FileInfo が含まれていない Apache 環境では .htaccess で圧縮を有効化することができません。

httpd.conf
AllowOverride 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 静的サイトを高速化する手順は以上です。

この記事を共有しませんか?