IISの静的圧縮でJavaScriptのgzipが壊れる原因と安全な対策まとめ

IIS 上で JavaScript をランタイムミニファイしていると、なぜか一部環境だけ「JS が途中で切れている」「gzip をオフにしたはずなのに壊れた .gz が返ってくる」といった不可解な挙動に悩まされることがあります。本記事では、IIS の静的圧縮キャッシュとランタイム生成 JS の相性問題を整理しながら、根本原因・再発防止策・暫定回避策をまとめて解説します。

目次

IIS で JavaScript が壊れた状態で圧縮配信される問題とは

まずは、今回の前提条件とよくある症状を整理しておきます。

  • JavaScript ファイルを ランタイムでミニファイ(再生成) している
    • アプリケーション起動時やアプリプールのリサイクル時に JS を結合・圧縮し直す
    • 生成先は IIS からそのまま配信するパス(例: /Scripts/app.min.js)
  • IIS 側では 静的圧縮(Static Compression)が有効 になっている
  • アプリプールのリセット/デプロイのタイミングで、まれに以下が発生する
    • 生成された JavaScript が途中で途切れている
    • ブラウザのコンソールに Unexpected end of input などの JS エラーが出る
    • F12 のネットワークタブで見ると、Content-Encoding: gzip が付いているのにサイズがやけに小さい

原因を一言でいうと、

「ファイルを書き換えている最中に、IIS の静的圧縮が走ってしまい、不完全な内容を .gz としてキャッシュしてしまう」

という レースコンディション です。

さらにややこしいのが、次のような疑問が出てくる点です。

  • 動的圧縮をオフにしたのに、なぜかまだ gzip が返ってくる
  • 圧縮をオフにしたあとも、IIS の一時フォルダーに .gz ファイルが残っているように見える
  • この挙動は IIS のバグなのか、それとも運用の問題なのか

以下では、IIS の静的圧縮と動的圧縮の挙動を整理しつつ、それぞれの疑問に答えていきます。

IIS の静的圧縮と動的圧縮の仕組み

IIS の圧縮機能は、大きく分けて次の 2 種類があります。

  • 静的圧縮(Static Compression) … 静的ファイル(.js, .css, .html など)を圧縮してキャッシュする
  • 動的圧縮(Dynamic Compression) … ASP.NET / MVC / Web API / PHP など、動的生成レスポンスをリアルタイムに圧縮する

両者の違いを表にまとめると、以下のようになります。

項目静的圧縮動的圧縮
対象物理ファイル(.js, .css, .html など)ASP.NET, PHP, MVC, Web API など動的コンテンツ
キャッシュ場所%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files基本はキャッシュせず、毎回圧縮(設定により挙動は変化)
圧縮トリガークライアントが Accept-Encoding: gzip, deflate を送ってきたとき同上
有効化/無効化doStaticCompression フラグdoDynamicCompression フラグ
生成タイミング初回アクセス時にメインスレッドで圧縮し、一時フォルダーに .gz を保存リクエストごとにその場で圧縮(=生成と同時)

重要なポイントは次の 2 点です。

  • 静的圧縮と動的圧縮は完全に別スイッチ であること
  • 静的圧縮は一時フォルダーに .gz をキャッシュして再利用する こと

つまり、動的圧縮だけを無効にしても、静的圧縮が有効であれば、キャッシュ済みの .gz はそのまま配信され続ける という挙動になります。

よくある疑問への回答

圧縮無効時に新しい .gz は生成されるのか?

結論から言うと、設定として圧縮を無効にしている状態で、新しい .gz が生成されることはありません。

  • doStaticCompression="false" かつ doDynamicCompression="false" の場合
    • クライアントが Accept-Encoding を送ってきても、IIS 側で圧縮処理は行われません
    • 一時フォルダー内のキャッシュは「更新」されません

ただし、ここでハマりがちなのは、過去に生成された .gz が一時フォルダーに残ったままになっている ケースです。設定を切り替えたタイミングやサイトのバインド設定などによっては、以下のようなことが起こり得ます。

  • 圧縮有効時に生成された .gz が一時フォルダーに存在
  • 設定を変えたが、別のサイトや別アプリケーションプールと共有されている
  • ブラウザ側キャッシュと組み合わさって挙動が分かりにくく見える

基本的には「圧縮を無効にしたあとの新規リクエストで .gz が生成されることはない」と考えて問題ありませんが、

  • トラブルシュートや環境切り替え時には、一時フォルダーの中身をクリアしておく

ことで、過去の遺物に振り回されるリスクを下げられます。

静的圧縮も無効にすべきか?

今回のように「ランタイムで JS を再生成する」運用をしている場合、静的圧縮も含めて完全に無効化するほうが安全 です。

設定例は次の通りです。

<system.webServer>
  <urlCompression doStaticCompression="false"
                  doDynamicCompression="false" />
</system.webServer>

この設定により、IIS は JS/CSS/HTML を含め、いっさい gzip 圧縮を行わなくなります。

もちろん、デメリットとして「転送量が増える」「初回表示が遅くなる」などがありますが、

  • JS が壊れてアプリ自体が動かなくなる

という致命的なリスクに比べると、十分に許容できるトレードオフです。

どうしても圧縮は維持したい場合は、後述するように、

  • ビルド時ミニファイ+ハッシュ付きファイル名に切り替える
  • ランタイム生成ファイルだけ圧縮対象から除外する

といった設計を検討しましょう。

web.config で設定するか、IIS マネージャーで設定するかで違いはある?

実質的には、どちらから設定しても最終的に反映される内容は同じ です。

  • IIS マネージャーから GUI で設定する
    • 内部的には applicationHost.config や該当サイトの web.config が書き換えられる
  • web.config を手動編集する
    • 管理者が直接 XML を編集するだけで、結果として IIS マネージャーに反映される

運用面での違いとしては、

観点IIS マネージャーweb.config 編集
変更の見える化GUI で現状はわかるが履歴は残しにくいGit などでバージョン管理しやすい
環境間の再現性本番・検証で同じ設定を再現しづらい同じ web.config をデプロイすれば再現しやすい
権限サーバー管理者のみが変更可能アプリ担当者でも変更可能(権限設計に依存)

トラブルを減らしたい場合は、

  • web.config に圧縮設定を明示的に書き、ソースコードと一緒に管理する

ことをおすすめします。

既知の不具合か? それとも運用上の問題か?

今回のケースは、一般的には IIS のバグではなく、運用設計の問題(レースコンディション) と捉えられます。

つまり、次のような前提がそろったときにだけ発生します。

  • 静的圧縮が有効になっている
  • 圧縮対象の JS ファイルを、配信パス上で直接書き換えている
  • ちょうどそのタイミングで、Accept-Encoding: gzip を付けたリクエストが来る

このとき、IIS は「今あるファイル」を信じて圧縮を開始するため、

  • 書き込み途中の不完全な JS が .gz として一時フォルダーに保存される
  • その後のリクエストには、壊れた .gz がキャッシュから返され続ける

という挙動になります。

静的圧縮の実装はかなり長い間変わっておらず、Microsoft 側にも「静的圧縮が JS を壊す既知のバグ」という扱いはほとんどありません。多くの事例は、

  • ファイルを配信パスで直接書き換えている
  • アプリ側の再生成タイミングと IIS の圧縮タイミングが競合している

という運用起因のトラブルとされています。

なぜ壊れた .gz が生成されるのか(レースコンディションの流れ)

もう少し細かく、問題が発生する流れを追ってみましょう。

  1. アプリ側が app.min.js を再生成し始める(旧バージョンの JS を上書き)
  2. 書き込み途中のタイミングで、ブラウザから app.min.js へのリクエストが到達
  3. IIS はその時点の app.min.js を読み込み、静的圧縮を行う
  4. 不完全な内容のまま gzip された .gz が一時フォルダーに保存される
  5. 以後のリクエストには、この壊れた .gz がキャッシュから返され続ける

ブラウザ側からは、

  • ステータスコードは 200 OK
  • ヘッダーには Content-Encoding: gzip
  • 実際に展開された JS は途中で終わっている

という、非常に紛らわしい状態になります。

このようなレースコンディションは、IIS に限らず Nginx や Apache といった他の Web サーバーでも起こり得るため、

  • 配信パス上のファイルを「書き換える」設計自体を避ける

ことが、もっとも確実な対策になります。

根本解決策:ビルド時ミニファイ+ハッシュ付きファイル名に切り替える

本質的な解決策は、次の 2 点です。

  1. JS のミニファイをランタイムではなくビルド時に行う
  2. 生成されたファイルにはハッシュ付きの名前を付け、配信パス上では「書き換えない」

具体的には、次のような運用にします。

  • CI/CD パイプライン(ビルド)で
    • 複数の JS をバンドル&ミニファイ
    • 内容に応じたハッシュを計算し、app.min.abcdef12.js のようなファイル名にする
  • アプリケーションの HTML テンプレートやビューでは、ハッシュ付きのファイル名を参照
  • 新バージョンのデプロイ時には、新しいファイル名を追加し、古いファイルは一定期間後に削除

この設計のメリットを整理すると、次のようになります。

項目ランタイムミニファイビルド時ミニファイ+ハッシュ
ファイル書き換えリスク配信パスの JS を上書きするためレースコンディションが発生しうる新しいファイル名を追加するだけなので、既存ファイルは壊れない
キャッシュ制御ファイル名が変わらないため、キャッシュのクリアが難しいファイル名にハッシュが含まれるため、Cache-Control: max-age=31536000 なども安全に設定可能
IIS 圧縮との相性書き換えと圧縮が競合しやすい生成済みファイルは変更されないため、圧縮キャッシュと安全に共存できる
運用のわかりやすさ「いつどのタイミングで再生成されるか」が把握しづらいビルドとデプロイのタイミングに限定されるため追跡しやすい

Webpack / Rollup / esbuild / Gulp など、モダンなフロントエンドビルドツールはこのスタイルを前提に設計されています。既存の IIS アプリケーションでも、ビルドパイプラインを整備できるなら、

  • ランタイム生成はやめて、ビルド時ミニファイ+ハッシュ付きファイル名へ移行する

ことを強くおすすめします。

それでもランタイム生成が必要な場合の安全なパターン

とはいえ、「ソースコードがバラバラでビルドパイプラインを組むのが大変」「ユーザーごとに JS の内容が変わる」など、どうしてもランタイム生成をやめられないケースもあります。その場合は、次のような対策を組み合わせると、リスクを大きく下げられます。

一時ファイルに書き出してから rename する(原子切り替え)

もっとも重要なのは、配信パス上のファイルを直接上書きしない ことです。代わりに、次のような手順でファイルを入れ替えます。

  1. 一時ファイル(例: app.min.js.tmp)として別名で書き出す
  2. 書き込み完了後、File.Move や rename で app.min.js に名前を変更する
  3. Windows では rename はほぼ原子的に行われるため、読み手側は旧版か新版のどちらかしか見ない

擬似コード例(C#)は次のようになります。

var target = @"C:\inetpub\wwwroot\Scripts\app.min.js";
var temp   = target + ".tmp";

File.WriteAllText(temp, minifiedJs, Encoding.UTF8);

// 既存ファイルを置き換える(原子操作)
File.Delete(target);
File.Move(temp, target);

この方式であれば、IIS が静的圧縮を開始するタイミングであっても、

  • 旧ファイルが完全な状態で存在する
  • ある瞬間に一気に新ファイルに切り替わる

ため、「途中までしか書き込まれていない JS を圧縮してしまう」という事態を避けられます。

対象 JS を圧縮対象から除外する

どうしても配信パスを書き換える必要がある場合は、その JS だけでも IIS の圧縮対象から外すことを検討します。例えば、該当ファイルを /dynamic-scripts/ 配下に集約し、そのパスだけ静的圧縮を無効化する、といったアプローチです。

例として、/dynamic-scripts/ 以下を静的圧縮から除外するイメージです(構成例)。

<location path="dynamic-scripts">
  <system.webServer>
    <urlCompression doStaticCompression="false" />
  </system.webServer>
</location>

もしくは、MIME タイプ単位で除外することもできます。例えば、JavaScript(application/javascript や text/javascript)の静的圧縮を無効にし、それ以外のファイルは圧縮する、といった設定も可能です。

<system.webServer>
  <httpCompression>
    <staticTypes>
      <add mimeType="application/javascript" enabled="false" />
      <add mimeType="text/javascript"        enabled="false" />
      <!-- それ以外の静的タイプは有効のまま -->
    &lt;/staticTypes&gt;
  &lt;/httpCompression&gt;
&lt;/system.webServer&gt;

このように「ランタイム生成 JS だけ圧縮しない」ようにしておけば、少なくとも不完全な .gz がキャッシュされるリスクはなくなります。

アプリプール再起動時に圧縮キャッシュを削除する

暫定策として有効なのが、アプリプールの再起動や JS 再生成の直後に、一時フォルダーの圧縮キャッシュを削除する 方法です。

対象ディレクトリは通常、次のパスです。

  • %SystemDrive%\inetpub\temp\IIS Temporary Compressed Files

ここをクリアすると、次回アクセス時に改めて .gz が生成されるため、「壊れた .gz が延々と配信される」状態からは脱出できます。

ただし、「書き換え中に再度壊れた .gz が生成される」根本要因までは解決しないため、あくまで 暫定的な応急処置 として位置付けるべきです。

圧縮キャッシュ削除の自動化例(PowerShell)

運用でよくあるのは、「アプリケーションのデプロイスクリプト」「アプリプール再起動バッチ」の中に、圧縮キャッシュ削除処理を組み込むパターンです。PowerShell であれば、以下のようなスクリプトで削除できます。

$tempPath = "$env:SystemDrive\inetpub\temp\IIS Temporary Compressed Files"

if (Test-Path $tempPath) {
    Write-Host "Deleting IIS temporary compressed files in $tempPath"
    Get-ChildItem $tempPath -Recurse -Force | Remove-Item -Force -Recurse
} else {
    Write-Host "Path not found: $tempPath"
}

これをデプロイの最後に実行しておけば、

  • 古いバージョンの .gz が残っているせいで不具合が出る

といったトラブルをかなり防げます。

ただし、

  • 一時フォルダーのファイル削除には適切な権限が必要
  • 共有サーバーなどで他サイトのキャッシュまで消してしまわないよう注意

といった点には気を付けてください。

トラブルシューティング時に確認すべきポイント

すでに「JS が壊れている」状態に遭遇してしまった場合、原因切り分けのために次の項目を確認すると効果的です。

確認ポイントチェック内容
レスポンスヘッダーContent-Encoding: gzip が付いているか、Content-Length の値は妥当か
ブラウザの Network タブ問題の JS のサイズ、ステータスコード、キャッシュ状況(from disk cache / memory cache など)を確認
.gz の実体一時フォルダーから該当 .gz をコピーし、ローカルで展開して中身が途中で途切れていないか確認
アプリ側の生成タイミングいつ、どのタイミングで JS を再生成しているか(起動時/リクエストごと/Cron など)
IIS の圧縮設定doStaticCompression と doDynamicCompression の値、対象 MIME タイプを確認

これらを確認することで、

  • ブラウザキャッシュの問題か
  • IIS の圧縮キャッシュの問題か
  • アプリ側の JS 生成ロジックの問題か

を切り分けやすくなります。

まとめ:まずは圧縮の無効化、将来的には運用設計の見直しを

最後に、本記事のポイントを整理します。

  • 圧縮が無効なら新しい .gz は生成されない
    • doStaticCompression="false" と doDynamicCompression="false" の場合、IIS は圧縮処理を行いません
    • ただし、過去の .gz が一時フォルダーに残っていることはあるので、必要に応じてクリアする
  • 静的圧縮と動的圧縮は別スイッチ
    • 動的圧縮を切っても静的圧縮が有効なら、静的ファイルの .gz はそのまま配信されます
    • 完全に止めるなら、両方を false にするか、JS を圧縮対象から外す必要があります
  • web.config でも IIS マネージャーでも結果は同じだが、web.config 管理が推奨
    • IIS マネージャーは内部的に構成ファイルを書き換えているだけです
    • 設定をコードと一緒に Git 管理できるという意味で、web.config への明示的記述が運用しやすいです
  • 問題の本質は IIS のバグではなく「ファイルを書き換える運用」
    • 静的圧縮自体が JS を壊しているのではなく、「書き込み途中のファイルを圧縮してしまう」レースコンディションが原因です
  • 根本解決は「ビルド時ミニファイ+ハッシュ付きファイル名」に移行すること
    • 配信パス上のファイルを上書きしない設計にすることで、圧縮キャッシュとの競合を根本からなくせます
  • ランタイム生成を続けるなら、原子的な rename と圧縮キャッシュ削除でリスクを下げる
    • 一時ファイルに書き出してから rename することで、「途中までしか書き込まれていないファイル」を配信しない
    • アプリプール再起動時に IIS Temporary Compressed Files をクリアしておくと、壊れた .gz が長期間残るのを防げます

「JS が壊れているけれど、ログにもエラーは出ていない」「gzip をオフにしたはずなのに、なぜかまだ壊れた JS が返ってくる」といった症状に心当たりがある場合は、まずは圧縮機能を完全にオフにして症状が止まるかどうかを確認し、そのうえで本記事で紹介したような ビルドプロセスと配信設計の見直し を進めていくのが近道です。

IIS の静的圧縮は適切に使えば非常に強力な機能ですが、ランタイム生成 JS との相性だけは要注意です。この記事をきっかけに、自身の環境の JS 配信設計を一度見直してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次