最新のブラウザに最新のコードを提供し、ページの読み込みを高速化する

この Codelab では、ユーザーがランダムに表示される猫を評価できるシンプルなアプリケーションのパフォーマンスを改善します。トランスパイルされるコードの量を最小限に抑えて JavaScript バンドルを最適化する方法について説明します。

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

サンプルアプリでは、単語または絵文字を選択して、各猫がどれくらい好きかを伝えることができます。ボタンをクリックすると、現在の猫の画像の下にボタンの値が表示されます。

測定

最適化を行う前に、まずウェブサイトを検査することをおすすめします。

  1. サイトをプレビューするには、[View App] を押します。次に、[Fullscreen] 全画面表示 を押します。
  2. Ctrl+Shift+J キー(Mac の場合は Command+Option+J キー)を押して DevTools を開きます。
  3. [ネットワーク] タブをクリックします。
  4. [Disable cache] チェックボックスをオンにします。
  5. アプリを再読み込みします。

元のバンドルサイズのリクエスト

このアプリケーションでは 80 KB 以上が使用されています。バンドルの一部が使用されていないかどうかを確認します。

  1. Control+Shift+P(Mac の場合は Command+Shift+P)を押して、[Command] メニューを開きます。 コマンド メニュー

  2. 「Show Coverage」と入力して Enter を押し、[Coverage] タブを表示します。

  3. [Coverage] タブで、[Reload] をクリックして、カバレッジをキャプチャしながらアプリケーションを再読み込みします。

    コード カバレッジを使用してアプリを再読み込みする

  4. メインバンドルで、使用されたコードの量と読み込まれたコードの量を比較してみましょう。

    バンドルのコード カバレッジ

バンドルの半分以上(44 KB)は使用されていません。これは、アプリケーションが古いブラウザで動作するように、コードの多くがポリフィルで構成されているためです。

@babel/preset-env を使用する

JavaScript 言語の構文は、ECMAScript(ECMA-262)と呼ばれる標準に準拠しています。仕様の新しいバージョンは毎年リリースされ、提案プロセスを通過した新機能が含まれています。主要なブラウザはそれぞれ、これらの機能のサポート状況が異なります。

このアプリケーションでは、次の ES2015 機能が使用されています。

次の ES2017 機能も使用されます。

src/index.js のソースコードを調べて、これらがどのように使用されているかを確認してください。

これらの機能はすべて最新バージョンの Chrome でサポートされていますが、サポートされていない他のブラウザではどうでしょうか。アプリケーションに含まれている Babel は、新しい構文を含むコードを古いブラウザや環境で理解できるコードにコンパイルするために使用される最も一般的なライブラリです。これは次の 2 つの方法で行われます。

  • Polyfills は、新しい ES2015+ 関数をエミュレートするために含まれているため、ブラウザでサポートされていなくても API を使用できます。以下に、Array.includes メソッドのポリフィルの例を示します。
  • プラグインは、ES2015 コード(またはそれ以降)を古い ES5 構文に変換するために使用されます。これらは構文関連の変更(アロー関数など)であるため、ポリフィルでエミュレートすることはできません。

package.json を調べて、どの Babel ライブラリが含まれているかを確認します。

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core は、コア Babel コンパイラです。これにより、すべての Babel 構成がプロジェクトのルートにある .babelrc で定義されます。
  • babel-loader は、webpack ビルドプロセスに Babel を含めます。

次に、webpack.config.js を確認して、babel-loader がルールとしてどのように含まれているかを確認します。

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill は、新しい ECMAScript 機能に必要なすべてのポリフィルを提供し、それらの機能をサポートしていない環境でも動作できるようにします。src/index.js. の最上部ですでにインポートされています。
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env は、ターゲットとして選択されたブラウザまたは環境に必要な変換とポリフィルを特定します。

Babel 構成ファイル .babelrc を確認して、どのように含まれているかを確認します。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

これは Babel と webpack の設定です。webpack 以外のモジュール バンドラを使用している場合は、アプリケーションに Babel を組み込む方法をご覧ください。

.babelrc の targets 属性は、ターゲットとするブラウザを識別します。@babel/preset-env は browserslist と統合されています。つまり、このフィールドで使用できる互換性のあるクエリの完全なリストは、browserlist のドキュメントで確認できます。

"last 2 versions" 値は、すべてのブラウザの最新の 2 つのバージョン用にアプリケーションのコードをトランスパイルします。

デバッグ

ブラウザの Babel ターゲットと、含まれているすべての変換とポリフィルをすべて確認するには、.babelrc: に debug フィールドを追加します。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • [ツール] をクリックします。
  • [ログ] をクリックします。

アプリケーションを再読み込みし、エディタの下部にある Glitch のステータスログを確認します。

対象ブラウザ

Babel は、コードがコンパイルされたすべてのターゲット環境など、コンパイル プロセスに関する詳細をコンソールに記録します。

対象ブラウザ

Internet Explorer などの廃止されたブラウザがこのリストに含まれていることに注意してください。これは、サポートされていないブラウザには新しい機能が追加されず、Babel がそれらのブラウザ用に特定の構文をトランスパイルし続けるため、問題となります。ユーザーがこのブラウザを使用してサイトにアクセスしていない場合、バンドルのサイズが不必要に大きくなります。

Babel は、使用された変換プラグインのリストもログに記録します。

使用されているプラグインのリスト

かなり長いリストです。これらはすべて、Babel がすべての対象ブラウザで ES2015+ 構文を古い構文に変換するために使用する必要があるプラグインです。

ただし、Babel は使用されている特定のポリフィルを表示しません。

ポリフィルが追加されていません

これは、@babel/polyfill 全体が直接インポートされているためです。

ポリフィルを個別に読み込む

デフォルトでは、@babel/polyfill がファイルにインポートされると、Babel は完全な ES2015+ 環境に必要なすべてのポリフィルをインクルードします。ターゲット ブラウザに必要な特定のポリフィルをインポートするには、構成に useBuiltIns: 'entry' を追加します。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

アプリケーションを再読み込みします。これで、含まれている特定のポリフィルをすべて確認できます。

インポートされたポリフィルのリスト

"last 2 versions" に必要なポリフィルのみが含まれるようになりましたが、それでも非常に長いリストです。これは、すべての新しい機能のターゲット ブラウザに必要なポリフィルがまだ含まれているためです。属性の値を usage に変更して、コードで使用されている機能に必要なもののみを含めます。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

これにより、必要に応じてポリフィルが自動的に追加されます。つまり、src/index.js. の @babel/polyfill インポートを削除できます。

import "./style.css";
import "@babel/polyfill";

これで、アプリケーションに必要なポリフィルのみが含まれるようになりました。

自動的に含まれるポリフィルのリスト

アプリケーション バンドルのサイズが大幅に縮小されます。

バンドルサイズが 30.1 KB に縮小

サポート対象ブラウザのリストの絞り込み

含まれるブラウザ ターゲットの数は依然として非常に多く、Internet Explorer などの廃止されたブラウザを使用するユーザーは多くありません。構成を次のように更新します。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

取得したバンドルの詳細を確認します。

バンドルサイズ: 30.0 KB

アプリケーションが非常に小さいため、これらの変更による違いはほとんどありません。ただし、ブラウザのマーケット シェア率(">0.25%" など)を使用し、ユーザーが使用していないことが確実な特定のブラウザを除外することをおすすめします。詳しくは、James Kyle 氏による 「Last 2 versions」considered harmful の記事をご覧ください。

<script type="module"> を使用する

まだ改善の余地はあります。未使用のポリフィルは多数削除されていますが、一部のブラウザでは不要なポリフィルが多数提供されています。モジュールを使用すると、不要なポリフィルを使用せずに、新しい構文を記述してブラウザに直接送信できます。

JavaScript モジュールは、すべての主要なブラウザでサポートされている比較的新しい機能です。モジュールは、type="module" 属性を使用して作成し、他のモジュールからインポートおよびエクスポートするスクリプトを定義できます。次に例を示します。

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

新しい ECMAScript 機能の多くは、JavaScript モジュールをサポートする環境ですでにサポートされています(Babel は必要ありません)。つまり、Babel 構成を変更して、2 つの異なるバージョンのアプリケーションをブラウザに送信できます。

  • モジュールをサポートする新しいブラウザで動作するバージョン。このバージョンには、ほとんどトランスパイルされていないものの、ファイルサイズが小さいモジュールが含まれています。
  • あらゆるレガシー ブラウザで動作する、トランスパイルされた大きなスクリプトを含むバージョン

Babel で ES モジュールを使用する

2 つのバージョンのアプリケーションに別々の @babel/preset-env 設定を適用するには、.babelrc ファイルを削除します。Babel の設定を webpack の構成に追加するには、アプリケーションのバージョンごとに 2 つの異なるコンパイル形式を指定します。

まず、以前のスクリプトの構成を webpack.config.js に追加します。

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

"@babel/preset-env" に targets 値を使用する代わりに、false 値を持つ esmodules が使用されていることに注意してください。つまり、Babel には、ES モジュールをまだサポートしていないすべてのブラウザをターゲットとするために必要なすべての変換とポリフィルが含まれています。

entry、cssRule、corePlugins オブジェクトを webpack.config.js ファイルの先頭に追加します。これらはすべて、ブラウザに提供されるモジュール スクリプトとレガシー スクリプトの両方で共有されます。

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

同様に、legacyConfig が定義されているモジュール スクリプトの下に構成オブジェクトを作成します。

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

主な違いは、出力ファイル名に .mjs ファイル拡張子が使用されることです。ここでは esmodules の値が true に設定されています。これは、このモジュールに出力されるコードが、より小さく、コンパイルが少ないスクリプトであることを意味します。この例では、使用されているすべての機能がモジュールをサポートするブラウザですでにサポートされているため、変換は行われません。

ファイルの最後に、両方の構成を 1 つの配列でエクスポートします。

module.exports = [
  legacyConfig, moduleConfig
];

これにより、サポートされているブラウザ用の小さなモジュールと、古いブラウザ用の大きなトランスパイルされたスクリプトの両方がビルドされます。

モジュールをサポートするブラウザは、nomodule 属性を持つスクリプトを無視します。逆に、モジュールをサポートしていないブラウザは、type="module" を含むスクリプト要素を無視します。つまり、モジュールだけでなく、コンパイル済みのフォールバックも含めることができます。理想的には、アプリケーションの 2 つのバージョンは、次のように index.html に存在する必要があります。

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

モジュールをサポートするブラウザは、main.mjs を取得して実行し、main.bundle.js. を無視します。モジュールをサポートしないブラウザは、その逆を行います。

通常のスクリプトとは異なり、モジュール スクリプトはデフォルトで常に遅延されることに注意してください。同等の nomodule スクリプトも遅延させ、解析後にのみ実行する場合は、defer 属性を追加する必要があります。

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

最後に行う必要があるのは、モジュールと以前のスクリプトにそれぞれ module 属性と nomodule 属性を追加することです。webpack.config.js の一番上で ScriptExtHtmlWebpackPlugin をインポートします。

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

次に、構成の plugins 配列を更新して、このプラグインを追加します。

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

これらのプラグイン設定により、すべての .mjs スクリプト要素に type="module" 属性が追加され、すべての .js スクリプト モジュールに nomodule 属性が追加されます。

HTML ドキュメントでモジュールを配信する

最後に行う必要があるのは、レガシー スクリプト要素と最新のスクリプト要素の両方を HTML ファイルに出力することです。残念ながら、最終的な HTML ファイルを作成するプラグイン HTMLWebpackPlugin は、現在、module スクリプトと nomodule スクリプトの両方の出力に対応していません。この問題を解決するために作成された回避策や個別のプラグイン(BabelMultiTargetPlugin や HTMLWebpackMultiBuildPlugin など)はありますが、このチュートリアルでは、モジュール スクリプト要素を手動で追加するというよりシンプルなアプローチを使用します。

ファイルの末尾の src/index.js に以下を追加します。

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

次に、モジュールをサポートするブラウザ(Chrome の最新バージョンなど)にアプリケーションを読み込みます。

新しいブラウザでは、5.2 KB のモジュールがネットワーク経由で取得されます。

フェッチされるのはモジュールのみで、大部分がトランスパイルされていないため、バンドルサイズははるかに小さくなります。もう一方のスクリプト要素は、ブラウザによって完全に無視されます。

古いブラウザでアプリケーションを読み込むと、必要なポリフィルと変換をすべて含む、トランスパイルされた大きなスクリプトのみが取得されます。古いバージョンの Chrome(バージョン 38)で行われたすべてのリクエストのスクリーンショットを以下に示します。

古いブラウザ用に 30 KB のスクリプトが取得される

まとめ

ここまでで、@babel/preset-env を使用して、ターゲット ブラウザに必要なポリフィルのみを提供する方法を学習しました。また、JavaScript モジュールで、アプリケーションの 2 つの異なるトランスパイル バージョンを配布することで、パフォーマンスをさらに向上させる方法についても学びました。この 2 つの手法でバンドルサイズを大幅に削減する方法を十分に理解したら、最適化を進めましょう。