コード圧縮(minify)完全ガイド:CSS・JS・HTML
コードの圧縮(minify)とは、CSS・JavaScript・HTML のソースから機械が必要としない文字、つまり空白・コメント・改行を取り除き、冗長な書き方をより短い等価な形に書き換える処理である。動作はそのままに、ファイルだけが小さくなり、読み込みが速くなる。
minify は compression(圧縮転送)とは別物だ。minify はソースコードそのものを対象にし、構文上の冗長さを削ぎ落とす。gzip と brotli は転送中のバイト列を対象にし、繰り返されるパターンを符号化する。両者は異なる段階で動き、異なる種類の冗長さに対処し、互いの上に積み重なる。だからサーバーがすでに brotli を配信していても、なお minify すべきなのだ。本ガイドではその理由を説明する。
今すぐ何かを圧縮したいなら、直接 CSSフォーマッター、JavaScript ミニファイア、HTML ミニファイア へ飛んでほしい。いずれもすべてブラウザ内だけで動作する。だが仕組みを理解してこそ、どこで圧縮するか、そしてそもそも手作業でやる必要があるのかを判断できるようになる。本ガイドでは、minify が実際に何をするのか、CSS・JS・HTML がそれぞれどう minify されるのか、minify が gzip と brotli とどう積み重なるのか、ビルドツールがすでに処理してくれるのはどんなときか、そして source map がどうやって minify 済みコードをデバッグ可能に保つのかを扱う。
minify とは何か(そして何ではないか)
minify は 2 つのことをする。パーサーにとって意味を持たない文字を削除し、まったく同じ意味を持つより短い形にソースを書き換えるのだ。出力は機械にとって完全に等価で、人間にとってはほぼ判読不能になる。コードの動き方は変わらず、変わるのは見た目だけだ。
この点は、本ガイド全体を通じて覚えておくべき不変条件である。minify が編集するのはソースの表面、つまり空白・コメント・識別子名・冗長な構文だけであり、振る舞いや出力には手を付けない。これは整形(formatting)の鏡像だ。整形はコードを読みやすくするために空白を足し、minify はコードを小さくするためにそれを削ぎ落とす。どちらも同じ「意味的に等価」という軸の上にあり、向きが逆を指しているだけである。
似た響きの 3 つの操作を、人は絶えず混同する。この表が整理してくれる。
| 観点 | 整形(beautify) | minify | 圧縮(gzip/brotli) |
|---|---|---|---|
| 何を変えるか | 空白・改行・インデントを追加 | 空白とコメントを除去し、構文を短縮 | 繰り返しパターンのバイトレベル符号化 |
| どの層か | ソースコード | ソースコード | 転送 / 保存 |
| まだソースコードか? | はい(読める) | はい(実行可能、読みづらい) | いいえ(バイナリ、復号が必要) |
| 誰がやるか | 開発者 / エディタ | ビルドツール / ミニファイア | サーバー + ブラウザ |
| 元に戻せるか? | 意味的に戻る | 意味的に戻る(振る舞いは不変) | 完全に戻る(解凍すればバイトが復元される) |
整形と minify は意味的等価性という 1 つの軸の上に存在する。圧縮はまったく別の軸に存在する。整形済みファイルも minify 済みファイルもどちらも有効なソースだが、圧縮済みファイルは何かを実行する前に復号しなければならないバイナリの塊だ。
ここで高くつく誤解が忍び込む。「うちのサーバーはもう gzip をかけているから、minify は無意味だ」というものだ。実際にはそうではない。その理由は本ガイドの後半に出てくる数字が示している。minify と compression は異なる冗長さを取り除くので、片方をやってももう片方が不要になるわけではない。各言語を見ていくあいだ、この点を頭に置いておいてほしい。
ミニファイアが取り除くバイトが、そもそもなぜ存在するのかを考えると理解が深まる。空白・コメント・説明的な名前は、自分自身とチームメイトのために書くものであり、コードをレビュー可能で保守可能にする。だが CSS を解析し、JavaScript を実行し、DOM を構築する機械は、そのどれひとつ見向きもしない。minify とは、人間がソースを書き終えたあとで、人間専用の材料を捨てる工程なのだ。だからこそ minify は*本番(production)*の関心事であって、開発の関心事ではない。読みやすい版はリポジトリに残し、削ぎ落とした版をブラウザへ届ける。読みやすいコピーが信頼できる唯一の元(source of truth)であり、minify 済みのコピーはいつでも再生成できるビルド成果物にすぎない。
CSS の minify はどう動くか
CSS は 3 つの中で最も穏やかに minify できる。文法に曖昧さの余地がほとんどないからだ。ミニファイアはコメントを取り除き、連続する空白を消し去り、各ブロックの末尾のセミコロンを落とし、{・}・:・; の周りの空白を除去する。それだけでほとんどのバイトが片付く。
CSS には、他のどの言語も持たない等価な書き換えがいくつか許されている。優れたミニファイアはそれらを安全に適用する。
- 色を短くする。
#ffffffは#fffになり、#ff0000はredに縮む(あるいは逆に、書いて短いほうへ)。 - ゼロの単位を落とす。
0pxは0になり、margin: 0 0 0 0はmargin: 0になる。 - 先頭のゼロを削る。
0.5emは.5emになる。 - ショートハンドにまとめる。
margin-top・margin-right・margin-bottom・margin-leftの 4 つの宣言は 1 つのmarginに畳まれる。 - ルールを結合する。 同一のセレクタや宣言を持つ隣接ルールはまとめられ、重複した宣言は落とされる。
これらはどれも描画結果を同一に保つ。それが、規格に準拠したミニファイアが越えない境界線だ。だが CSS は順序に敏感である。後のルールはカスケードを通じて前のルールを上書きする。だから安全なミニファイアは、どの宣言が勝つかを変えてしまいかねないルールの並べ替えを盲目的には行わない。バイトを縮めるのは許されるが、カスケードを変えるのは許されない。
この制約は、聞こえるよりも繊細だ。一見まとめられそうな 2 つの宣言が、実はまとめられないことがある。その間に同じプロパティを同じ詳細度で参照する何かが挟まっているからだ。次を見てほしい。
.btn { color: #ff0000; }
.alert .btn { color: blue; }
.btn { color: #f00; }
1 番目と 3 番目のルールはセレクタを共有しており、まとめられる。ただし、両方にマッチする要素についてどちらが勝つかを変えるような形で、宣言を真ん中のルールの向こう側へ動かさない場合に限る。これらを並べ替えてしまう素朴なマージはカスケードを壊しかねない。これこそ CSSO のような本番品質のエンジンが論理立てて扱うために作られたエッジケースであり、自前の「空白を消すだけ」のミニファイアを正規表現で書くべきでない理由でもある。変換は機械的に見えるが、その裏にある安全性の分析はそうではない。
私たちの CSSフォーマッター は、この種のロスレスな minify のために CSSO エンジンを使っており、すべてブラウザ内で動作し、各パスのペイロード影響が分かるようバイト削減量を表示する。同じツールは逆方向の整形もこなすので、稼働中のサイトからコピーした minify 済みスタイルシートを、読みやすくインデントの付いたルールへ展開し直せる。CSS のスニペットをコピーして圧縮後のサイズを確認したいとき、あるいはビルド工程のない静的ページを公開するときに、手に取ってほしい。
JavaScript の minify はどう動くか
JavaScript の minify は CSS よりずっと踏み込む。そこにこそ、節約と落とし穴の両方が潜んでいる。小さな関数を Terser の前後で見比べてみよう。
// 圧縮前
function calculateTotal(items, taxRate) {
let runningTotal = 0;
for (const item of items) {
runningTotal += item.price * item.quantity;
}
return runningTotal * (1 + taxRate);
}
// 圧縮後
function calculateTotal(t,a){let n=0;for(const o of t)n+=o.price*o.quantity;return n*(1+a)}
関数名 calculateTotal が生き残るのは、エクスポートされている(あるいは他所から呼ばれうる)からだ。引数とループ変数は 1 文字に縮む。それが核心だが、JS ミニファイアはいくつもの異なることをする。
- 識別子のマングリング。 ローカル変数と引数は 1 文字にリネームされる。
getUserPreferencesはaになる。マングルされるのはローカルだけで、グローバルやエクスポートされた名前は既定でそのまま残る。リネームすると外部からそれを参照するコードが壊れてしまうからだ。 - デッドコード除去。 到達不能な分岐や未使用の変数が取り除かれ、バンドラー層の tree-shaking と手を携えて働く。
- 定数畳み込みと構文圧縮。 式が短くされる。
trueは!0に、falseは!1に、return undefined;はreturn;になる。
JS の minify について最も価値のある知識は、自動セミコロン挿入(ASI)の罠だ。JavaScript はセミコロンの省略を許し、パーサーが特定の規則のもとであなたの代わりに挿入する。ミニファイアがその規則の依拠する改行を削除すると、コードの意味が変わりうる。典型的な失敗は、( または [ で始まる文が、こっそり前の行に貼り付けられてしまうことだ。
const x = getValue()
[1, 2, 3].forEach(handle)
セミコロンがないと、これは getValue()[1, 2, 3]、つまりインデックス参照式であって、2 つの文ではないものとして解釈される。ひとたび 1 行に minify されると、バグはそのまま固定される。同じ危険は ( で始まる行でも現れ、前の式が関数のように呼び出されてしまう。現代の Terser はほとんどの実世界のケースをうまく処理する。コードをまず抽象構文木(AST)に解析し、必要な場所にセミコロンを再出力するからで、盲目的なテキスト削除をしているわけではない。だが、まずいソースと攻撃的な minify の組み合わせは本番バグの原因になり、しかもその失敗は minify 済みビルドにしか現れず開発時には現れないからこそ、たちが悪い。直す責任はあなたの側にある。明示的なセミコロンと曖昧さのない構文でコードを書けば、ミニファイアは安全なままだ。ソースレベルでセミコロンを挿入する linter ルールや自動フォーマッターを使えば、リスクは消える。
準拠したミニファイアは振る舞いを保つ。ただし入力が有効で標準的な JavaScript である場合に限る。Terser は ECMAScript を解析するが、TypeScript も JSX も理解しない。それらはまずプレーンな JS にトランスパイルしておかなければならず、さもなければ minify は解析の段階で失敗する。.ts ファイルを JS ミニファイアに貼り付けてエラーになるなら、理由はこれだ。
命名の疑問がよく出る。minify と uglify はどう違うのか、と。実質的に同じものを指す。「uglify」は、初期に普及した JS ミニファイアである UglifyJS に由来する。Terser はその現代版フォークで、ES2015 以降をサポートする。今日では「minify」が 3 言語すべてにまたがる一般的な用語であり、「uglify」は同一の処理を指す古い、JS 固有の呼び名として生き残っている。
私たちの JavaScript ミニファイア は Terser をブラウザ内で動かし、ローカルをリネームし、デッドコードを落とし、コメントを取り除いたうえで、各パスで何バイト節約したかを報告する。
HTML の minify はどう動くか
HTML の minify は基本から始まる。コメントを取り除き(<!DOCTYPE> 宣言と、まだ頼りにしている条件付きコメントは残す)、タグ間の空白を消し、属性リスト内の冗長な空白を切り詰める。小さな断片がその形を見せてくれる。
<!-- ナビゲーション -->
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
これが次のようになる。
<ul><li><a href=/>Home</a><li><a href=/about>About</a></ul>
コメントが消え、タグ間のインデントが消え、省略可能な </li> 終了タグが落ち、引用符の要らない属性値はその引用符を失う。そこからさらに、ミニファイアは HTML 固有のいくつかの技を適用できる。
- 省略可能な終了タグを取り除く。 HTML 仕様は
</li>・</p>・</td>その他いくつかの省略を許すので、ミニファイアはそれらを落とせる。 - 属性の引用符を取り除く。 値に空白や特殊文字がないとき、
class="x"はclass=xになる。 - 真偽値属性を畳む。
disabled="disabled"はただのdisabledになり、checked="checked"はcheckedになる。 - 埋め込まれた CSS と JS を minify する。
<style>と<script>ブロックの中身も minify されるので、1 回のパスで文書全体が縮む。
最も重要な境界線はこれだ。HTML では空白が意味を持つことがある。<pre> と <textarea> の中では、すべての空白と改行がそのまま描画される。white-space: pre を持つ要素も同じように振る舞う。そしてインライン要素の間の空白はレイアウトに影響し、2 つの <a> タグの間の空白は、ページ上では隙間として現れる。この空白を平坦化する攻撃的な minify は、ページの見た目を変えうる。経験則はこうだ。minify したあとは、公開する前に pre・textarea・インライン要素の境界まわりの描画を確認すること。
私たちの HTML ミニファイア は js-beautify で整形し、埋め込まれたスタイルとスクリプトを CSSO と Terser で minify する。すべてクライアントサイドだ。圧縮してくれるビルド工程がめったに存在しない、メール用 HTML や CMS が書き出したマークアップに便利だ。
minify vs gzip vs brotli——どう積み重なるか
中心的な問いに移ろう。サーバーがすでに gzip や brotli を配信しているなら、それでも minify は必要なのか? 必要だ。その理由は、2 つの技法が異なる冗長さを取り除くからである。
minify はソースレベルの構文的冗長さを取り除く。人間の可読性のために存在する空白・コメント・長い名前・冗長な構文だ。gzip と brotli はバイトレベルの統計的冗長さを取り除く。ファイル中で繰り返される文字列やパターンが、より短いコードに置き換えられる。一方はコードの構文を理解し、もう一方はただバイトの流れを見るだけだ。両者は異なるものを狙うので、積み重ねがよく効く。
具体的に見てみよう。gzip は、バンドル内で function という文字列が 200 回現れることに気づき、各出現を短い後方参照に置き換えるのが得意だ。だが getUserPreferences と getUserSettings が短くできる変数名であることや、if (false) { ... } ブロック全体が決して走らないことには、まったく気づかない。minify が扱うのはまさにそこ、つまりバイトレベルの圧縮器には見えない、構造的・意味的な勝ち筋だ。両者を一緒に走らせれば、それぞれが相手に見えないものを片付ける。
実際に起きる順序のとおりに、数字を並べよう。
- minify 単独は通常、空白とコメントを除去し構文を短縮することで、CSS・JS・HTML を 20〜30% 縮める。
- minify 済み出力への gzip は、テキストに残る繰り返しパターンを符号化することで、さらに 60〜80% を取り除く。
- gzip の代わりに brotli を使うと、より大きな組み込み辞書とより優れたアルゴリズムのおかげで、出力はさらに 15〜25% 小さくなる。
まとめると、まず minify、それから compress。組み合わせた結果は、元のソースより 80〜90% 小さくなることが多い。どちらかを飛ばせば、節約できたはずのバイトをそのまま残すことになる。
brotli の上でもなお minify が働きに見合う理由は 3 つある。
- 小さい入力はより小さく圧縮される。minify 済みファイルは、圧縮器に噛み砕くべき冗長な材料を減らして渡す。小さくきれいな入力は、一般により小さな出力を生む。
- minify は圧縮にできないことをする。デッドコード除去と短い変数名は意味的な除去だ。gzip はあなたのコードを理解せず、バイトしか見えないので、未使用の関数を削除したり変数をリネームしたりはできない。
- ブラウザが解析するバイトが減る。解凍後、ブラウザは minify 済みのコードを受け取る。コードが少なければ、ダウンロードが小さくなるだけでなく、解析と実行も速くなる。
この順序は、各ステップがどこに属するかから自然に決まる。minify はビルド時に属し、あなたかビルドツールが一度だけ行う。compression は転送時に属し、サーバーがリクエストごとに行い、ブラウザが到着時に解凍する。だからパイプラインは自然と minify → デプロイ → サーバーが圧縮、となる。逆向きには走らせられない。「圧縮してから minify」する方法は存在しない。圧縮済みの出力はもうソースコードではないからだ。
「まず minify、それから compress」には小さいが重要な但し書きがある。いったん圧縮済みになった内容を再び圧縮するのは無意味か、むしろ逆効果だ。すでにバイナリで高エントロピーのアセット、たとえば JPEG・PNG・WebP・WOFF2 のフォントは gzip や brotli から何も得られず、そもそもテキスト圧縮の対象に入れるべきでない。minify はテキスト専用の変換なので、そうしたファイルには触れない。選り分けが必要になるのは compression のほうだ。サーバーはテキスト系の MIME タイプ(HTML・CSS・JS・JSON・SVG)を圧縮するよう設定し、すでに圧縮済みのバイナリはそのままにしておこう。
転送層の設定、つまり brotli の有効化や Content-Encoding の指定は、サーバーや CDN が扱う運用(ops)の関心事だ。本ガイドは minify が起きるソース層に留まる。ペイロードをより広く最適化しているなら、同じ「符号化層でバイトを節約する」考え方は画像にも当てはまる。私たちの 画像フォーマットガイド が WebP/AVIF/JPEG 側を扱っている。
手作業で minify しなくてよいとき
多くのミニファイアの宣伝が飛ばしてしまう真実がある。ビルド工程があるなら、本番の出力はすでに minify 済みだ。現代のビルドパイプラインは自動でそれをやる。
Vite と esbuild は、JavaScript と CSS を最初から minify する。Rollup と webpack は TerserPlugin と CssMinimizerPlugin を通じてそれを行う。Lightning CSS はネイティブ速度で CSS を扱う。Next.js・Astro やそれに類するフレームワークは、あなたが指一本動かさずとも、本番ビルドで minify し、tree-shake し、チャンクを分割する。コマンドはたいてい vite build か npm run build 程度のものだ。minify は「本番用にビルドする」ことの一部であって、後から付け足す別の工程ではない。これがあなたのプロジェクトに当てはまるなら、後からファイルを別のミニファイアに通すのは、良くて冗長、悪くて有害だ。すでに minify 済みのコードを二重にマングルすると、紛らわしい出力を生み、意味のある追加バイトも節約できない。
ビルドツールはまた、単体のミニファイアにできないこともする。依存グラフ全体の文脈の中で minify するのだ。とりわけ tree-shaking は、バンドラーがすべての import と export を見渡し、ある関数が決して使われないと証明できて初めて働く。単一ファイルのミニファイアには論理立てて扱うグラフがなく、渡されたファイルの中のデッドコードは落とせても、import されたモジュール全体が到達不能だとは判断できない。これも、本番の minify の正しい住処がビルドパイプラインである理由のひとつだ。
では、単体のミニファイアが正しい道具なのはどんなときか? それを代わりにやってくれるビルド工程が存在しないケースだ。
- 静的サイトと手書きの単一ファイルページで、バンドラーが関与しないもの。
- メール用 HTML テンプレート。多くのシステムがバイト単位で課金し、ビルドパイプラインがまったく存在しない。
- サードパーティのスニペットやウィジェットコードで、他人のページに埋め込むもの。
- 手早いサイズ確認。ブロックを貼り付け、minify 後にどれだけの大きさになり、どれだけ節約できたかを見る。バイト削減量の表示はそのためにある。
- 他人の minify 済みコードを読むとき。フォーマッターを逆向きに走らせて、再び読めるようにする。
判断は単純だ。ビルドがあるなら、minify はビルドに任せればいい。ビルドがない、一度きり、あるいはただサイズを確認したいだけなら、オンラインツールが最速の道だ。これらのツールはすべてブラウザ内で動くので、コードがあなたの端末を離れることはない。これは、独自の、あるいは未公開のコードにとって重要だ。すべてのコピーを受け取るサーバーサイドのフォーマッターに、そういうコードを貼り付けてはいけない。同じプライバシーの論拠は、このクラスターのもう一つの整形の深掘りである私たちの SQL スタイルガイド にも貫かれている。
source map——minify 済みコードのデバッグ
minify 済みのコードは、それだけではデバッグの悪夢だ。ひとたび Terser がすべてのローカル変数を a・b・c にリネームしてしまうと、bundle.min.js:1:48211 を指す本番のスタックトレースは、実際に何が壊れたのかをほとんど何も教えてくれない。
source map がこれを解決する。source map とは、minify 済み出力の各位置と、対応する元のソースの位置との対応を記録した .map ファイルだ。ブラウザの DevTools がそれを読み込むと、minify 済みのエラーを本物のファイル名・行番号・変数名へと翻訳し戻す。ブラウザが動かしているのはビルドが生み出したコードであっても、あなたは自分が書いたコードに対してデバッグできる。
実際には、ビルドツールが minify 済みバンドルと並んで source map を生成し、//# sourceMappingURL=bundle.min.js.map というコメント(または HTTP ヘッダー)がブラウザを .map へ導く。DevTools を開き、エラーに当たれば、スタックトレースは minify されたスープではなく、本物のファイル名と行番号を見せてくれる。マップは遅延読み込みされ、DevTools が開いているときだけ読まれるので、訪問者には何のコストもかからない。
知っておく価値のあるプライバシーの側面がある。公開された source map は、事実上、DevTools を開く誰に対してもあなたの元のソースコードを配ってしまう。公開コードなら構わないが、独自コードならそうはいかない。そのためにあるのが隠し(hidden)source map だ。バンドルは sourceMappingURL コメントを持たないので一般の人がマップを見ることはなく、それでもあなたは Sentry のようなエラー監視サービスへそれをアップロードする。サービスが自分の側で本番スタックトレースを de-minify し、ソースを世界にさらすことなく読めるエラーを与えてくれる。
この点も先の話を補強する。source map はビルドツールの機能だ。素のオンラインミニファイアは普通それを生成しない。一度きりの圧縮には必要ないからだ。これも、本番の minify をビルドに任せるべき理由のひとつで、マップが無料で手に入る。そして、source map が minify 済みバンドルそのものを変えることはない。それはその傍らに置かれたデバッグ補助でしかない。.map を本番の依存物と取り違えてはいけない。
よくある質問
minify は compression(圧縮)と同じものですか?
いいえ。minify はソースコードを書き換え、空白とコメントを取り除き、名前を短くするので、有効なコードのまま、ただ小さくなる。compression(gzip・brotli)はその結果のバイトを転送のために符号化し、ブラウザがそれを復号する。両者は異なる冗長さを攻め、異なる段階で働き、積み重なる。まず minify、それから compress。
gzip や brotli を使っていれば minify は不要ですか?
いいえ、必要だ。gzip や brotli を使っていても minify は依然として重要だ。minify 済みコードは圧縮器に与える冗長な入力を減らすので、より小さく圧縮される。さらに minify は、デッドコード除去や短い変数名のように、バイトレベルの圧縮にはできない意味的な除去を行う。ブラウザが解析するバイトも減る。両方を、その順序で使おう。
minify でコードが壊れることはありますか?
準拠したミニファイアは振る舞いを保つ。CSS は同一に描画され、Terser は JavaScript を等価に保つので、出力はソースと同じように動く。注意が 2 つある。自動セミコロン挿入に頼る JavaScript には有効な構文が必要で、<pre> や <textarea> のような空白に敏感な HTML は minify 後に確認すべきだ。
minify と uglify の違いは何ですか?
JavaScript については実質的に同じものを指す。「uglify」は初期に普及した JS ミニファイアである UglifyJS に由来する。Terser はその現代版フォークで、現行の構文をサポートする。今日では「minify」が CSS・JS・HTML をまたいで一般的に使われ、「uglify」は同じ処理を指す古い、JS 固有の呼び名だ。
開発中に minify すべきですか?
いいえ。minify するのは本番ビルドであって、開発ではない。minify 済みコードは判読不能でデバッグしづらいので、開発中は完全で整形されたソースが欲しい。ビルドツール(Vite・esbuild・webpack)は本番用にビルドするとき自動で minify し、しばしば source map も付けてくれるので、デプロイされたバンドルでもデバッグできる。
minify でファイルサイズはどれくらい減りますか?
minify 単独では通常、主に空白とコメントを除去し名前を短くすることで、CSS・JS・HTML を約 20〜30% 縮める。その上に gzip や brotli を重ねると、組み合わせた結果は元のソースより 80〜90% 小さくなることが多い。正確な数字は、そのファイルがどれだけの空白と冗長さを抱えていたかによる。