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 のヘッダーで

お気軽にご相談ください

お見積りへ お問い合わせへ
▲