compression について
Brotli、Gzip、ミニファイ、静的・動的圧縮を扱う、JavaScriptとHTTP圧縮の開発スキルです。転送サイズと読み込み性能を改善したい開発者向けに、圧縮とバンドル粒度、キャッシュの兼ね合いも説明します。
主な機能
- 01BrotliとGzipの比較
- 02静的・動的圧縮の選択
- 03圧縮前のJavaScriptミニファイ
- 04チャンクサイズとキャッシュの調整
- 05DevToolsで圧縮を確認
活用シーン
- 01JavaScriptの転送サイズ削減
- 02低速接続での読み込み改善
- 03静的・動的コンテンツの圧縮設定
- 04DevToolsで圧縮レスポンスを確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill compressionインストール後、Claude Code で/compressionを入力して読み込まれたか確認します。
詳細
JavaScript の圧縮
目次
JavaScript は、インターネット上で画像に次いで、ページサイズへの寄与度が 2 番目に大きく、リクエスト数の多いウェブリソースとしても 2 番目です。ウェブサイトのパフォーマンスを向上させるため、JavaScript の転送、読み込み、実行にかかる時間を短縮するパターンを採用します。圧縮によって、スクリプトをネットワーク経由で転送するのに必要な時間を短縮できます。
使用する場面
- ページの読み込みを高速化するために、JavaScript のペイロードサイズを削減する必要がある場合に使用します
- ネットワーク転送時間の最適化に役立ちます。特に、接続速度が遅いユーザーに有効です
- ミニファイ、コード分割、キャッシュ戦略と併用します
手順
- 同程度の速度でより高い圧縮率が得られる Brotli を、Gzip より優先します
- 頻繁には変更されないアセットには静的圧縮を、頻繁に変更されるコンテンツには動的圧縮を使用します
- サーバーまたは CDN レベルで圧縮を有効にします(例:Nginx、Vercel、Netlify)
- 圧縮を適用する前に JavaScript をミニファイします
- 粒度のトレードオフに注意します。大きなバンドルは圧縮率が高くなりますが、小さなチャンクはキャッシュ効率が高くなります
詳細
圧縮は、ミニファイ、コード分割、バンドル、キャッシュ、遅延読み込みなどのほかの手法と組み合わせて、大量の JavaScript がパフォーマンスに与える影響を軽減できます。ただし、これらの手法の目的が相反する場合もあります。このセクションでは、JavaScript の圧縮手法を説明し、コード分割と圧縮の戦略を決める際に考慮すべき細かな点を取り上げます。
- JavaScript の圧縮には Gzip と Brotli が最も広く使われており、最新のブラウザーで広くサポートされています。
- Brotli は、同程度の圧縮レベルで より高い圧縮率 を実現します。
- Next.js ではデフォルトで Gzip 圧縮が提供されていますが、Nginx などの HTTP プロキシで有効にすることが推奨されています。
- Webpack でコードをバンドルする場合、Gzip 圧縮には CompressionPlugin、Brotli 圧縮には BrotliWebpackPlugin を使用できます。
- Oyo では 15~20%、Wix では Gzip から Brotli 圧縮に切り替えた後、ファイルサイズが 21~25% 削減されました。
- compress(a + b) <= compress(a) + compress(b) - 大きなバンドルを 1 つにまとめると、小さなバンドルを複数用意するよりも圧縮率が高くなります。このため、重複排除やキャッシュと、ブラウザーのパフォーマンスや圧縮との間で粒度のトレードオフが生じます。細かくチャンク化することで、このトレードオフへの対処に役立ちます。
HTTP 圧縮
圧縮すると、ドキュメントやファイルのサイズが小さくなり、元のファイルよりもディスク領域を節約できます。サイズの小さいドキュメントは帯域幅の消費量も少なく、ネットワーク経由で素早く転送できます。HTTP 圧縮はこのシンプルな考え方を使ってウェブサイトのコンテンツを圧縮し、ページの容量を減らして、必要な帯域幅を抑え、パフォーマンスを向上させます。
HTTP データ圧縮は、さまざまな方法で分類できます。そのひとつが、非可逆圧縮と可逆圧縮です。
非可逆圧縮では、圧縮と展開のサイクルによって、使いやすさを保ちながらドキュメントがわずかに変化します。この変化は、エンドユーザーにはほとんど気づかれません。非可逆圧縮の最も一般的な例は、画像の JPEG 圧縮です。
可逆圧縮では、圧縮してから展開した後に復元されるデータが、元のデータと完全に一致します。PNG 画像は可逆圧縮の例です。可逆圧縮はテキストの転送に適しており、HTML、CSS、JavaScript などのテキストベースの形式に適用する必要があります。
ブラウザー上で有効な JavaScript コードをすべて利用できるようにするには、JavaScript コードに可逆圧縮アルゴリズムを使用する必要があります。JavaScript を圧縮する前に、ミニファイによって不要な構文を取り除き、実行に必要なコードだけに減らせます。
ミニファイ
ペイロードのサイズを減らすには、圧縮の前に JavaScript をミニファイできます。ミニファイは、空白や不要なコードを取り除いて、サイズを小さくしつつ有効性を保ったコードファイルを作成することで、圧縮を補完します。コードを書くときは、読みやすさと保守性を高めるために、改行、インデント、スペース、わかりやすい変数名、コメントを使います。しかし、これらの要素は JavaScript 全体のサイズを増やす一方で、ブラウザーでの実行には必要ありません。ミニファイによって、JavaScript コードを正常に実行するために必要な最小限の量まで減らせます。
ミニファイは、JS と CSS の最適化における標準的な手法です。JavaScript ライブラリの開発者が、本番環境へのデプロイ用にミニファイ済みのファイルを提供するのは一般的で、通常はファイル名の拡張子に min.js を付けます(例:jquery.js と jquery.min.js)。
HTML、CSS、JS リソースのミニファイに利用できるツールは複数あります。Terserは ES6 以降に対応した人気の JavaScript 圧縮ツールで、Webpack v4 には、ミニファイ済みのビルドファイルを作成するための、このライブラリのプラグインがデフォルトで含まれています。古いバージョンの Webpack では TerserWebpackPlugin も使用できます。また、モジュールバンドラーを使わずに、Terser を CLI ツールとして使用することもできます。
静的圧縮と動的圧縮
ミニファイによってファイルサイズを大幅に削減できますが、JS を圧縮すると、さらに大きな効果が得られます。サーバー側の圧縮は、次の 2 つの方法で実装できます。
静的圧縮: 静的圧縮を使用すると、リソースを事前に圧縮し、ビルドプロセスの一環として保存できます。この場合、コードのダウンロード時間を短縮するために、より高い圧縮レベルを使用できます。ビルド時間が長くなっても、ウェブサイトのパフォーマンスには影響しません。あまり頻繁に変更されないファイルには、静的圧縮を使用するのが最適です。
動的圧縮: この方法では、ブラウザーがリソースをリクエストしたときに、その場で圧縮が行われます。動的圧縮は実装が簡単ですが、使用できる圧縮レベルは低くなります。圧縮レベルを高くすると時間がかかり、コンテンツサイズの縮小によって得られるメリットが失われます。頻繁に変更されるコンテンツや、アプリケーションによって生成されるコンテンツには、動的圧縮を使用するとよいでしょう。
アプリケーションのコンテンツの種類に応じて、静的圧縮または動的圧縮を使用できます。一般的な圧縮アルゴリズムを使用して静的圧縮と動的圧縮の両方を有効にできますが、それぞれに推奨される圧縮レベルは異なります。
圧縮アルゴリズム
Gzip と Brotli は、現在 HTTP データの圧縮に使用されている 最も一般的なアルゴリズム です。
Gzip
Gzip 圧縮形式は約 30 年前から存在しており、Deflate アルゴリズムに基づく可逆アルゴリズムです。Deflate アルゴリズム自体は、入力データストリーム内のデータブロックに対して、LZ77 アルゴリズムとハフマン符号化を組み合わせて使用します。
LZ77 アルゴリズムは重複する文字列を特定し、バックリファレンスに置き換えます。バックリファレンスは、文字列が以前に現れた場所を示すポインターに、文字列の長さを続けたものです。その後、ハフマン符号化によって、よく使われる参照が特定され、より短いビット列の参照に置き換えられます。使用頻度の低い参照は、より長いビット列で表現されます。
主要なブラウザーはすべて Gzip をサポートしています。Zopfli 圧縮アルゴリズムは、より低速ですが改良された Deflate/Gzip のバージョンであり、Gzip 互換の、より小さなファイルを生成します。より大きな効果が得られる静的圧縮に最適です。
Brotli
2015 年、Google は Brotli アルゴリズムと Brotli 圧縮データ形式を発表しました。GZip と同様に、Brotli も LZ77 アルゴリズムとハフマン符号化に基づく可逆アルゴリズムです。さらに、2 次のコンテキストモデリングを使用することで、同程度の速度でより高密度な圧縮を実現します。コンテキストモデリングは、同じブロック内で同じアルファベットに対して複数のハフマン木を使用できる機能です。Brotli はバックリファレンス用により大きなウィンドウサイズもサポートし、静的辞書も備えています。これらの機能により、圧縮アルゴリズムとしての効率が高まります。
現在、Brotli は主要なサーバーやブラウザーすべてでサポートされており、人気も高まっています。また、Netlify、AWS、Vercelなどのホスティングプロバイダーやミドルウェアでもサポートされており、簡単に有効化できます。
OYO や Wix など、ユーザー数の多いウェブサイトでは、Gzip を Brotli に置き換えたことでパフォーマンスが大幅に向上しました。
Gzip と Brotli の比較
Gzip と Brotli を使った JavaScript の圧縮に関する Chrome の調査から、いくつかの知見を紹介します。
- Gzip 9 は優れた圧縮速度と最高の圧縮率を実現するため、他の Gzip レベルより先に使用を検討してください。
- Brotli では、レベル 6~11 を検討してください。それ以外の場合、Gzip を使えば同程度の圧縮率をはるかに速く実現できます。
- あらゆるサイズの範囲で Brotli 9~11 は Gzip よりはるかに優れた性能を発揮しますが、かなり低速です。
- バンドルが大きいほど、より優れた圧縮率と速度が得られます。
- アルゴリズム間の関係は、バンドルのサイズにかかわらず同様です(たとえば、どのバンドルサイズでも Brotli 7 は Gzip 9 より優れており、すべてのサイズ範囲で Gzip 9 は Brotli 5 より高速です)。
圧縮の有効化
ビルドの一環として静的圧縮を有効にできます。Webpack を使用してコードをバンドルする場合は、Gzip 圧縮には CompressionPlugin を、Brotli 圧縮には BrotliWebpackPlugin を使用できます。次のように、プラグインを Webpack の設定ファイルに含めることができます。
module.exports = {
//...
plugins: [
//...
new CompressionPlugin(),
],
};
Next.js はデフォルトで Gzip 圧縮を提供していますが、Nginx のような HTTP プロキシで有効にすることを推奨しています。Vercel プラットフォームでは、プロキシレベルで Gzip と Brotli の両方がサポートされています。
異なる圧縮アルゴリズムをサポートするサーバー(Node.js を含む)では、動的な可逆圧縮を有効にできます。ブラウザーは、リクエスト内の HTTP ヘッダー Accept-Encoding を通じて、サポートする圧縮アルゴリズムを伝えます。たとえば、Accept-Encoding: gzip, br です。
これは、ブラウザーが Gzip と Brotli をサポートしていることを示します。サーバーの種類に応じた手順に従って、サーバーでさまざまな圧縮方式を有効にできます。たとえば、Apache サーバーで Brotli を有効にする手順はこちらで確認できます。Express は Node 向けの人気のあるウェブフレームワークで、compression ミドルウェアライブラリを提供しています。これを使用すると、リクエストされたアセットを圧縮できます。
Brotli はファイルサイズをより小さくできるため、ほかの圧縮アルゴリズムよりも推奨されます。Brotli に対応していないブラウザー向けのフォールバックとして、Gzip を有効にできます。設定が正しく行われると、サーバーはレスポンスで使用した圧縮アルゴリズムを示す HTTP レスポンスヘッダー Content-Encoding を返します。たとえば、Content-Encoding: br です。
圧縮の監査
Chrome DevTools → Network → Headers で、サーバーがダウンロードされたスクリプトやテキストを圧縮したかどうかを確認できます。DevTools には、レスポンスで使用された content-encoding が表示されます。
Lighthouse レポートには「Enable Text Compression」のパフォーマンス監査が含まれています。この監査では、content-encoding ヘッダーが「br」、「gzip」、「deflate」のいずれにも設定されていない状態で受信された、テキストベースのリソースタイプを確認します。Lighthouse は、リソースの削減可能なサイズを計算する際に Gzip を使用します。
JavaScript の圧縮と読み込みの粒度
JavaScript の圧縮による影響を十分に理解するには、ルートベースの分割、コード分割、バンドルなど、JavaScript の最適化に関するほかの側面も考慮する必要があります。
JavaScript コードが大量にある最新のウェブアプリケーションでは、効率よくコードを読み込むために、さまざまなコード分割やバンドルの手法がよく使われます。アプリでは、Single Page Application のルートレベルの分割や、操作時またはビューポート内に表示されたときに JavaScript を段階的に配信するなど、論理的な境界を使ってコードを分割します。バンドラーは、これらの境界を認識するように設定できます。
バンドリングに関する用語
以下は、ここでの説明に関係する主な用語です。
- モジュール:モジュールは、堅牢な抽象化とカプセル化を提供するように設計された、機能の独立したまとまりです。詳しくは、Module pattern をご覧ください。
- バンドル:個別のモジュールをまとめたもので、ソースファイルの最終版を含み、バンドラーでの読み込みとコンパイルの処理をすでに経ています。
- バンドル分割:バンドラーを使ってアプリケーションを複数のバンドルに分割し、それぞれを独立して分離、公開、ダウンロード、またはキャッシュできるようにするプロセスです。
- チャンク:Webpack の用語に由来するチャンクは、バンドリングとコード分割の最終的な出力です。Webpack では、entry 設定、SplitChunksPlugin、または動的インポートに基づいて、バンドルをチャンクに分割できます。
ソースファイルにモジュールが含まれている場合、コード分割またはバンドル分割後のビルドプロセスの最終出力はチャンクと呼ばれます。ソースファイルとチャンクは、どちらも互いに依存する場合があります。
JavaScript の出力サイズとは、JavaScript バンドラーまたはコンパイラーによる最適化後のチャンクのサイズ、または未圧縮サイズを指します。大規模な JS アプリケーションは、個別に読み込み可能な JavaScript ファイルのチャンクに分割できます。読み込みの粒度とは、出力チャンクの数を指します。チャンク数が多いほど、各チャンクのサイズは小さくなり、粒度は高くなります。
読み込み頻度が高い、または影響の大きいコードパス(たとえば、「checkout」ウィジェットの読み込み)の一部であるなどの理由から、他のチャンクより重要なチャンクもあります。どのチャンクが最も重要かを把握するには、アプリケーションに関する知識が必要ですが、「base」チャンクは常に不可欠だと考えて差し支えありません。
ページに必要なチャンクは、すべてのバイトをユーザーデバイスにダウンロードし、解析・実行する必要があります。このコードは、アプリケーションのパフォーマンスに直接影響します。チャンクは最終的にダウンロードされるコードであるため、チャンクを圧縮するとダウンロード速度の向上につながります。
粒度のトレードオフ
理想的には、粒度とチャンク分割の方針は、互いに相反する次の目標を達成することを目指すべきです。
- ダウンロード速度の向上: 前のセクションで見たように、圧縮を利用するとダウンロード速度を向上できます。ただし、大きなチャンクを1つ圧縮するほうが、同じコードを複数の小さなチャンクに分けて圧縮するよりも、圧縮結果が良くなり、ファイルサイズも小さくなります。
compress(a + b) <= compress(a) + compress(b)
- キャッシュヒット率とキャッシュ効率の向上: サイズの小さなチャンクほどキャッシュ効率が高くなります。特に、JavaScriptを段階的に読み込むアプリでは効果的です。
-
チャンクが小さいほど、変更の影響を受けるチャンクの数を少なくできます。コードに変更があった場合、再ダウンロードが必要なのは影響を受けるチャンクだけであり、該当するコードのサイズも小さくなる可能性が高くなります。残りのチャンクはキャッシュから取得できるため、キャッシュヒット数が増加します。
-
チャンクが大きい場合、コードの広範囲に影響が及び、コード変更後に再ダウンロードが必要になる可能性が高くなります。
したがって、キャッシュの仕組みを活用するには、小さなチャンクが望ましいです。
- 高速な実行: コードを高速に実行するには、次の条件を満たす必要があります。
- 必要な依存関係をすぐに利用できること。つまり、まとめてダウンロードされているか、キャッシュ内にある必要があります。そのためには、関連するコードをすべてまとめて大きなチャンクにする必要があります。
- ページやルートに必要なコードだけが実行されること。これには、不要なコードがダウンロードも実行もされないことが求められます。共通の依存関係を含む
commonsチャンクには、ほとんどのページで必要でも、すべてのページで必要とは限らない依存関係が含まれる場合があります。コードの重複を排除するには、小さく独立したチャンクが必要です。 - メインスレッド上の長時間タスクは、スレッドを長時間ブロックする可能性があります。そのため、こうしたタスクは小さなチャンクに分割する必要があります。
上記の目標のいずれか1つを最適化する読み込み粒度を選ぶと、ほかの目標から遠ざかることがあります。これが粒度のトレードオフの問題です。
重複排除とキャッシュは、ブラウザーのパフォーマンスおよび圧縮と相反します。
このトレードオフのため、現在、多くの本番アプリで使用されているチャンクの最大数はおよそ10です。大量のJavaScriptを含むアプリで、より優れたキャッシュと重複排除を実現するには、この上限を引き上げる必要があります。
SplitChunksPluginとグラニュラーなチャンク分割
粒度のトレードオフに対する解決策では、次の要件に対応する必要があります。
- パフォーマンスに影響を与えずに、キャッシュと重複排除を改善するため、小さなチャンクサイズでより多くのチャンク(40〜100個)を許可する。
- 多数の小さなチャンクによって発生する、IPC、I/O、および多数のスクリプトタグの処理コストによるパフォーマンスのオーバーヘッドに対処する。
- 複数の小さなチャンクを使用した場合に発生する圧縮効率の低下に対処する。
これらの要件に対応する解決策は、現在も開発中です。しかし、Webpack v4のSplitChunksPluginとグラニュラーなチャンク分割戦略により、読み込みの粒度をある程度高めることができます。
以前のバージョンのWebpackでは、共通の依存関係や共有モジュールを単一のチャンクにまとめるためにCommonsChunkPluginを使用していました。その結果、これらの共通モジュールを使用しないページでも、ダウンロード時間と実行時間が不必要に増加することがありました。このようなページをより適切に最適化できるよう、Webpack v4ではSplitChunksPluginが導入されました。デフォルト設定または構成に基づいて複数の分割チャンクが作成され、さまざまなルート間でコードが重複して取得されるのを防ぎます。
Next.jsはSplitChunksPluginを採用し、粒度のトレードオフに対応するWebpackチャンクを生成するため、次のグラニュラーなチャンク分割戦略を実装しました。
- 十分なサイズのサードパーティモジュール(160 KB超)は、それぞれ個別のチャンクに分割されます。
- フレームワークの依存関係用に、別個のフレームワークチャンクが作成されます。(react、react-domなど)
- 必要に応じて共有チャンクが作成されます。(最大25個)
- チャンク生成の最小サイズが20 KBに変更されます。
単一の共有チャンクではなく複数の共有チャンクを出力することで、異なるページでダウンロードまたは実行される不要なコード(または重複コード)の量を最小限に抑えられます。大規模なサードパーティライブラリを独立したチャンクとして生成すると、頻繁に変更される可能性が低いため、キャッシュが改善されます。チャンクの最小サイズを20 kBにすることで、圧縮効率の低下を十分に抑えられます。
グラニュラーなチャンク分割戦略により、複数のNext JSアプリでサイト全体のJavaScript使用量が削減されました。同様の効果が確認されたこの戦略は、Gatsbyにも実装されました。
まとめ
圧縮だけですべてのJavaScriptパフォーマンスの問題を解決することはできませんが、ブラウザーやバンドラーが内部でどのように動作するかを理解すれば、より優れた圧縮を実現するバンドル戦略を作成できます。読み込みの粒度の問題には、エコシステム内のさまざまなプラットフォームで対処する必要があります。グラニュラーなチャンク分割はその方向への一歩かもしれませんが、まだ多くの課題が残っています。