gzip でネットワーク ペイロードを最小化して圧縮する

この Codelab では、次のアプリケーションの JavaScript バンドルを縮小して圧縮することで、アプリのリクエスト サイズを削減し、ページのパフォーマンスを改善する方法について説明します。

アプリのスクリーンショット

測定

最適化を追加する前に、アプリケーションの現在の状態を分析することをおすすめします。

  • サイトをプレビューするには、[アプリを表示] を押し、[全画面表示] 全画面表示 を押します。

このアプリは、「未使用のコードを削除する」の Codelab でも取り上げられており、お気に入りの子猫に投票できます。🐈

このアプリケーションのサイズを確認します。

  1. `Ctrl+Shift+J`(Mac の場合は `Command+Option+J`)を押して、デベロッパー ツールを開きます。
  2. [ネットワーク] タブをクリックします。
  3. [キャッシュを無効にする] チェックボックスをオンにします。
  4. アプリを再読み込みします。

ネットワーク パネルでの元のバンドルサイズ

「未使用のコードを削除する」の Codelab でこのバンドルサイズを縮小するために多くの進展がありましたが、225 KB はまだかなり大きなサイズです。

Minification

次のコードブロックについて考えてみましょう。

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

この関数を独自のファイルに保存する場合、ファイルサイズは約 112 B(バイト)です。

空白をすべて削除すると、コードは次のようになります。

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

ファイルサイズは約 83 B になります。変数名の長さを短縮し、一部の式を変更することでさらに難読化すると、最終的なコードは次のようになります。

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

ファイルサイズが 62 B に達しました。

ステップごとにコードが読みにくくなっています。ただし、ブラウザの JavaScript エンジンは、これらのそれぞれをまったく同じように解釈します。この方法でコードを難読化すると、ファイルサイズを小さくできます。112 B はもともとそれほど大きくありませんでしたが、それでもサイズが 50% 削減されました。

このアプリケーションでは、webpack バージョン 4 がモジュール バンドラとして使用されています。特定のバージョンは package.json で確認できます。

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

バージョン 4 では、本番環境モードでデフォルトでバンドルが最小化されます。Terser のプラグインである TerserWebpackPlugin を使用します。Terser は、JavaScript コードの圧縮に使用される一般的なツールです。

縮小化されたコードがどのようなものかを確認するには、DevTools の [ネットワーク] パネルで main.bundle.js をクリックします。[Response] タブをクリックします。

最小化されたレスポンス

最終的な形式のコード(最小化およびマングル処理済み)がレスポンス本文に表示されます。最小化されていない場合のバンドルのサイズを確認するには、webpack.config.js を開き、mode 構成を更新します。

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

アプリケーションを再読み込みし、DevTools の [ネットワーク] パネルでバンドルサイズを再度確認します。

バンドルサイズが 767 KB

これは大きな違いです。😅

続行する前に、ここで変更を元に戻してください。

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

アプリケーションにコードを縮小するプロセスを含めるかどうかは、使用するツールによって異なります。

  • webpack v4 以降を使用している場合は、本番環境モードでコードがデフォルトで最小化されるため、追加の作業は必要ありません。👍
  • 古いバージョンの webpack を使用している場合は、TerserWebpackPlugin をインストールして webpack ビルドプロセスに含めます。詳細については、ドキュメントをご覧ください。
  • BabelMinifyWebpackPluginClosureCompilerPlugin など、他の圧縮プラグインも存在し、代わりに使用できます。
  • モジュール バンドラーをまったく使用していない場合は、CLI ツールとして Terser を使用するか、依存関係として直接含めます。

圧縮

「圧縮」という用語は、ミニファイア プロセスでコードがどのように削減されるかを説明するために大まかに使用されることがありますが、実際には文字どおりの意味で圧縮されるわけではありません。

通常、圧縮とは、データ圧縮アルゴリズムを使用して変更されたコードを指します。完全に有効なコードを提供するミニファイケーションとは異なり、圧縮されたコードは使用前に解凍する必要があります。

ブラウザとウェブサーバーは、すべての HTTP リクエストとレスポンスでヘッダーを追加して、取得または受信されるアセットに関する追加情報を含めることができます。これは、DevTools の [Network] パネルの Headers タブで確認できます。このタブには、次の 3 種類のデータが表示されます。

  • General は、リクエストとレスポンスのやり取り全体に関連する一般的なヘッダーを表します。
  • [レスポンス ヘッダー] には、サーバーからの実際のレスポンスに固有のヘッダーのリストが表示されます。
  • [リクエスト ヘッダー] には、クライアントによってリクエストに付加されたヘッダーのリストが表示されます。

Request Headersaccept-encoding ヘッダーをご覧ください。

Accept-Encoding ヘッダー

accept-encoding は、ブラウザがサポートするコンテンツ エンコード形式または圧縮アルゴリズムを指定するために使用されます。テキスト圧縮アルゴリズムは数多く存在しますが、HTTP ネットワーク リクエストの圧縮(および解凍)でサポートされているのは次の 3 つのみです。

  • Gzipgzip): サーバーとクライアントのインタラクションで最も広く使用されている圧縮形式。Deflate アルゴリズムをベースにしており、現在のすべてのブラウザでサポートされています。
  • Deflate(deflate): 一般的には使用されません。
  • Brotlibr): 圧縮率をさらに高めることを目指した新しい圧縮アルゴリズム。ページ読み込みをさらに高速化できます。ほとんどのブラウザの最新バージョンでサポートされています。

このチュートリアルのサンプル アプリケーションは、Express がサーバー フレームワークとして使用されている点を除き、「未使用のコードを削除する」 Codelab で完成したアプリと同じです。以降のセクションでは、静的圧縮と動的圧縮の両方について説明します。

動的圧縮

動的圧縮では、ブラウザからリクエストされたアセットをその場で圧縮します。

長所

  • アセットの圧縮版を保存して更新する必要はありません。
  • オンザフライ圧縮は、動的に生成されるウェブページに特に効果的です。

短所

  • 圧縮率を高めるためにファイルを高いレベルで圧縮すると、時間がかかります。ユーザーがサーバーから送信される前にアセットが圧縮されるのを待つため、パフォーマンスが低下する可能性があります。

Node/Express での動的圧縮

server.js ファイルは、アプリケーションをホストする Node サーバーの設定を担当します。

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

現在、このコードは express をインポートし、express.static ミドルウェアを使用して public/ ディレクトリ内のすべての静的 HTML、JS、CSS ファイルを読み込むだけです(これらのファイルはビルドごとに webpack によって作成されます)。

アセットがリクエストされるたびに圧縮されるようにするには、圧縮ミドルウェア ライブラリを使用します。まず、package.jsondevDependency として追加します。

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

そして、サーバー ファイル server.js にインポートします。

const express = require('express');
const compression = require('compression');

また、express.static がマウントされるに、ミドルウェアとして追加します。

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

アプリを再読み込みして、[ネットワーク] パネルでバンドルサイズを確認します。

動的圧縮によるバンドルサイズ

225 KB から 61.6 KB になりました。Response Headers では、content-encoding ヘッダーは、サーバーが gzip でエンコードされたこのファイルを送信していることを示しています。

コンテンツ エンコード ヘッダー

静的圧縮

静的圧縮の背後にある考え方は、アセットを事前に圧縮して保存することです。

長所

  • 圧縮レベルが高いことによるレイテンシは、もはや問題ではありません。ファイルを圧縮するためにオンザフライで処理する必要はありません。ファイルを直接取得できるようになったためです。

短所

  • アセットはビルドごとに圧縮する必要があります。圧縮レベルが高い場合、ビルド時間が大幅に増加する可能性があります。

Node/Express と webpack を使用した静的圧縮

静的圧縮ではファイルを事前に圧縮するため、webpack の設定を変更して、ビルドステップの一部としてアセットを圧縮できます。この場合は CompressionPlugin を使用できます。

まず、package.jsondevDependency として追加します。

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

他の webpack プラグインと同様に、構成ファイル webpack.config.js: でインポートします。

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

plugins 配列内に含めます。

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

デフォルトでは、プラグインは gzip を使用してビルドファイルを圧縮します。別のアルゴリズムを使用するオプションを追加したり、特定のファイルを含めたり除外したりする方法については、ドキュメントをご覧ください。

アプリが再読み込みされて再ビルドされると、メインバンドルの圧縮バージョンが作成されるようになりました。Glitch Console を開いて、Node サーバーによって提供される最終的な public/ ディレクトリの中身を確認します。

  • [ツール] ボタンをクリックします。
  • [コンソール] ボタンをクリックします。
  • コンソールで次のコマンドを実行して、public ディレクトリに移動し、そのすべてのファイルを表示します。
cd public
ls

パブリック ディレクトリ内の最終出力ファイル

バンドルの gzip 圧縮バージョン main.bundle.js.gz もここに保存されるようになりました。CompressionPlugin は、デフォルトで index.html も圧縮します。

次に、元の JS バージョンがリクエストされたときに、これらの gzip 圧縮ファイルを送信するようにサーバーに指示する必要があります。これは、express.static でファイルが提供される前に、server.js で新しいルートを定義することで実現できます。

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get は、特定のエンドポイントに対する GET リクエストにサーバーがどのように応答するかを伝えるために使用されます。コールバック関数を使用して、このリクエストの処理方法を定義します。このルートは次のように機能します。

  • 最初の引数として '*.js' を指定すると、JS ファイルを取得するために起動されるすべてのエンドポイントでこの関数が機能します。
  • コールバック内で、.gz がリクエストの URL に付加され、Content-Encoding レスポンス ヘッダーが gzip に設定されます。
  • 最後に、next() は、シーケンスが次のコールバックに継続されるようにします。

アプリが再読み込みされたら、もう一度 Network パネルを確認します。

静的圧縮によるバンドルサイズの削減

以前と同様に、バンドルサイズが大幅に縮小されました。

まとめ

この Codelab では、ソースコードの最小化と圧縮のプロセスについて説明しました。これらの手法は、現在利用可能な多くのツールでデフォルトになりつつあります。そのため、ツールチェーンがすでにこれらの手法をサポートしているかどうか、または両方のプロセスを自分で適用し始める必要があるかどうかを確認することが重要です。