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

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

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

測定

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

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

このアプリは、「未使用のコードを削除する」Codelab でも説明したように、お気に入りの子猫に投票できます。🐈

このアプリケーションのサイズを見てみましょう。

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

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

「不要なコードを削除する」Codelab でこのバンドルサイズを削減するための多くの作業が進みましたが、225 KB はまだかなり大きいサイズです。

圧縮

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

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 をクリックします。[レスポンス] タブをクリックします。

圧縮されたレスポンス

最終的なコード(圧縮され、マングリングされたコード)がレスポンス本文に表示されます。バンドルが圧縮されていない場合のサイズを確認するには、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): 一般的に使用されません。
  • Brotlibr): 圧縮率をさらに改善することを目的とした新しい圧縮アルゴリズム。これにより、ページの読み込みがさらに高速化されます。ほとんどのブラウザの最新バージョンでサポートされています。

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

動的圧縮

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

長所

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

短所

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

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 Headerscontent-encoding ヘッダーは、サーバーが gzip でエンコードされたこのファイルを送信していることを示しています。

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

静的圧縮

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

長所

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

短所

  • アセットはビルドのたびに圧縮する必要があります。高い圧縮レベルが使用されている場合、ビルド時間が大幅に長くなることがあります。

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

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

まず、これを devDependency として package.json に追加します。

"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 コンソールを開き、Node サーバーが提供する最終的な public/ ディレクトリの内容を確認します。

  • [ツール] ボタンをクリックします。
  • [Console] ボタンをクリックします。
  • コンソールで次のコマンドを実行して 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 では、ソースコードを圧縮して圧縮するプロセスを説明しました。これらの手法は、現在利用可能な多くのツールでデフォルトになりつつあるため、ツールチェーンがすでにこれらの手法をサポートしているのか、それとも両方のプロセスを自分で適用する必要があるのかを確認することが重要です。