2026年6月18日
2026年6月18日
Apacheで静的ファイルを効率よく配信する設定:キャッシュ・圧縮・ETag・sendfile
Apacheで画像・CSS・JavaScriptなどの静的ファイルを速く配信するには、ブラウザにキャッシュさせる期間を正しく伝えること、テキスト系を圧縮すること、そして再検証(304応答)がきちんと働くようにすることの3点が中心になります。この記事では mod_expires・mod_headers・mod_deflate・mod_brotli の設定例と、ETagやEnableSendfileで起きやすい落とし穴、設定が効いているかの確かめ方を説明します(2026年10月現在のApache 2.4系を前提にしています)。
先にモジュールを有効にする
使うモジュールは mod_expires、mod_headers、mod_deflate(と mod_filter)、mod_brotli です。Debian/Ubuntu系では a2enmod で有効にします。
sudo a2enmod expires headers deflate filter brotli
sudo apachectl configtest
sudo systemctl reload apache2
RHEL系(AlmaLinux、Rocky Linuxなど)では /etc/httpd/conf.modules.d/ 配下の設定で読み込まれており、多くは最初から有効です。読み込まれているモジュールは次のコマンドで一覧できます。
apachectl -M | grep -E "expires|headers|deflate|brotli|filter"
設定は VirtualHost か conf ファイルに書くのが基本です。.htaccess でも書けますが、リクエストのたびにディレクトリを遡って読み込まれるため、サーバー設定を触れるならそちらのほうが軽くなります。共用サーバーなどで .htaccess しか使えない場合は、AllowOverride で該当ディレクティブが許可されている必要があります。
キャッシュ期間の考え方
静的ファイルのキャッシュ設定で大事なのは、「ファイル名やURLが変わらない限り中身も変わらないもの」と「同じURLで中身が変わるもの」を分けることです。
WordPressでは、テーマやプラグインのCSS・JavaScriptに ?ver=6.x のようなクエリ文字列が付き、更新時に値が変わります。こうしたファイルや、ハッシュ付きファイル名でビルドされたアセットは、長期間キャッシュさせても更新が伝わります。一方、HTMLや、同じファイル名のまま差し替える画像は、長くキャッシュさせると古い内容が見え続けます。
mod_expires で期間を指定する
mod_expires は、MIMEタイプごとに Expires ヘッダーと Cache-Control の max-age を付けます。
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 hour"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
access plus はアクセスした時点からの期間です。modification plus を使うとファイルの更新日時基準になりますが、PHPが出力するHTMLのように更新日時を持たないレスポンスには効きません。
つまずきやすいのはMIMEタイプの一致です。JavaScriptは環境によって application/javascript と text/javascript のどちらで返るかが違います。実際のレスポンスの Content-Type を確認してから書いてください。
mod_headers で Cache-Control を細かく制御する
mod_expires では public や immutable のような指定はできません。より細かく指定したい場合は mod_headers を使います。
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(html|htm)$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
immutable は「有効期間内は再検証しなくてよい」という意味で、リロード時の確認リクエストを減らせます。URLに版が付いているファイルだけに使います。no-cache は「キャッシュしてはいけない」ではなく「使う前に毎回サーバーへ確認する」という意味で、HTMLにはこちらが向いています。保存自体をさせたくない場合は no-store です。
mod_expires と mod_headers を同じファイルに両方適用すると、どちらの値が最終的に出ているのか分かりにくくなります。Header set は Cache-Control ヘッダーを置き換えますが、混乱を避けるため、ファイルの種類ごとにどちらか一方で管理するのがおすすめです。
mod_deflate と mod_brotli で圧縮する
テキスト系のファイルは圧縮で大きく縮みます。brotli は Apache 2.4.26 以降の mod_brotli で使え、gzip より高い圧縮率が期待できます。
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS;DEFLATE text/html text/plain text/css \
application/javascript text/javascript application/json \
application/xml image/svg+xml
</IfModule>
<IfModule !mod_brotli.c>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css \
application/javascript text/javascript application/json \
application/xml image/svg+xml
</IfModule>
</IfModule>
BROTLI_COMPRESS;DEFLATE はフィルターを順につなぐ書き方です。ブラウザが br を受け付けるなら brotli で圧縮され、受け付けない場合は gzip で圧縮されます。すでに Content-Encoding が付いたレスポンスは二重に圧縮されません。
対象に画像(JPEG、PNG、WebP)やwoff2、zipなどを含めないでください。もともと圧縮済みのため縮まず、CPUを使うだけです。AddOutputFilterByType は Apache 2.4 では mod_filter が必要で、無効だとエラーになります。また、Vary: Accept-Encoding はこれらのモジュールが自動で付けるため、途中のプロキシやCDNが圧縮版と非圧縮版を区別できるようになっています。
ETag の扱い
ETag はファイルの識別子で、ブラウザは If-None-Match で「手元のものと同じか」を確認し、同じなら304応答で本体の転送を省けます。Apache 2.4 の既定は FileETag MTime Size で、更新日時とサイズから作られます。
問題になりやすいのは次の2つです。
1つ目は、複数台のWebサーバーに負荷分散している場合です。デプロイ方法によってサーバーごとにファイルの更新日時がずれると、同じファイルでもETagが変わり、304が返らなくなります。
2つ目は圧縮との組み合わせです。mod_deflate や mod_brotli で圧縮したレスポンスには、ETagの末尾に -gzip や -br が付きます。これは圧縮版と非圧縮版を区別するための挙動ですが、条件付きリクエストとの照合がうまくいかず304にならないケースがあります。
こうした場合は、ETagを使わず Last-Modified による再検証に任せる方法があります。
FileETag None
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
ただし、ETag を外すと更新日時だけが頼りになります。単一サーバーで問題が出ていないなら、既定のままにしておくのが無難です。変える前に、ブラウザの開発者ツールで2回目の読み込みが304になっているかを確認してください。
EnableSendfile と EnableMMAP の注意点
EnableSendfile は、ファイルの中身をカーネル内で直接ソケットへ送る sendfile を使う設定で、Apache 2.4 の既定はOffです。Onにすると静的配信の負荷を下げられる場合があります。
EnableSendfile On
一方で、NFSやSMBなどのネットワークファイルシステム、VirtualBoxやVagrantの共有フォルダ上のファイルでは、古い内容や壊れたデータが返る不具合が知られています。ドキュメントルートがこうした場所にあるなら Off のままにします。EnableMMAP も同様の理由で、ネットワークファイルシステム上では Off が推奨されています。開発環境で「CSSを直したのに反映されない」ときは、この2つを疑ってください。
設定が効いているかを確認する
変更後は、構文チェックをしてから reload します。
sudo apachectl configtest && sudo systemctl reload apache2
レスポンスヘッダーは curl で確認できます。
# キャッシュヘッダーの確認
curl -sI https://example.com/wp-content/themes/your-theme/style.css \
| grep -iE "cache-control|expires|etag|last-modified"
# 圧縮の確認(br と gzip のそれぞれで)
curl -sI -H "Accept-Encoding: br" https://example.com/ | grep -i content-encoding
curl -sI -H "Accept-Encoding: gzip" https://example.com/ | grep -i content-encoding
CDNやキャッシュプラグインを使っている場合、表示されるヘッダーがApacheのものとは限りません。オリジンサーバーに直接アクセスして確認するか、CDN側の設定も合わせて見直します。本番の設定ファイルは変更前にバックアップを取っておき、問題が出たらすぐ戻せるようにしておいてください。
まとめ
- URLに版が付くCSS・JSは長期キャッシュ、HTMLは
no-cacheで毎回確認、と種類で分ける - mod_expires は期間指定、mod_headers は
publicやimmutableなど細かい指定。同じファイルに両方かけない - 圧縮は
BROTLI_COMPRESS;DEFLATEでbrotli優先、gzipにフォールバック。画像やwoff2は対象外 - ETag は複数台構成や圧縮で304が効かなくなることがある。症状を確認してから
FileETag Noneを検討 - EnableSendfile と EnableMMAP は、ネットワークファイルシステムや共有フォルダ上では Off にする
- 反映は
apachectl configtestのあと reload、確認は curl のヘッダーで