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

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

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

測定

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

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

このアプリでは、お気に入りの子猫に投票できます。このアプリは、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 バイトになります。変数名の長さを短くし、式を変更することで、さらに複雑化した場合、最終的なコードは次のようになります。

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 の [Network] パネルでバンドルサイズを再度確認します。

バンドル サイズ: 767 KB

かなり大きな違いです。😅

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

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

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

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

圧縮

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

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

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

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

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 によって作成されます)。

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

"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 を使用できます。

まず、これを 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 では、ソースコードを圧縮して圧縮するプロセスを説明しました。これらの手法は、現在利用可能な多くのツールでデフォルトになっているため、ツールチェーンがすでにこれらの手法をサポートしているかどうか、両方のプロセスを自分で適用する必要があるかどうかを確認することが重要です。