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

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

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

測定

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

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

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

次に、このアプリケーションの大きさを見てみましょう。

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

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

「Remove unused code」 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 の [Network] パネルを開いた状態で 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 をインストールしてビルドプロセスに組み込みます。詳しくは、ドキュメントをご覧ください。
  • 他の圧縮プラグイン(BabelMinifyWebpackPluginClosureCompilerPlugin など)も使用できます。
  • モジュール バンドラがまったく使用されていない場合は、Terser を CLI ツールとして使用するか、依存関係として直接含めます。

圧縮

「圧縮」という用語は、圧縮プロセス中にコードがどのように削減されるかを説明するために広く使われることもありますが、実際には文字どおりの圧縮ではありません。

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

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

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

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

エンコード ヘッダーを受け入れる

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

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

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

動的圧縮

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

長所

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

デメリット

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

Node/Express による動的圧縮

server.js ファイルは、アプリケーションをホストするノードサーバーの設定を行います。

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'));

//...

アプリを再読み込みし、[Network] パネルでバンドルサイズを確認します。

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

225 KB から 61.6 KB になりました。Response Headerscontent-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 コンソールを開いて、ノードサーバーから提供される最終的な 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 では、ソースコードの圧縮と圧縮のプロセスについて説明しました。これらの手法は、現在利用可能なツールの多くでデフォルトになりつつあります。そのため、ツールチェーンがこれらの手法をすでにサポートしているか、または両方のプロセスを自分で適用する必要があるかを確認することが重要です。