最新の JavaScript を公開、配布、インストールして、アプリケーションの高速化を実現

最新の JavaScript の依存関係と出力を有効にして、パフォーマンスを向上させます。

ブラウザの 90% 以上は最新の JavaScript を実行できますが、従来の JavaScript が広く使用されていることが、ウェブ上のパフォーマンスの問題の大きな原因となっています。

最新の JavaScript

最新の JavaScript は、特定の ECMAScript 仕様バージョンで記述されたコードではなく、すべての最新ブラウザでサポートされている構文で記述されたコードです。Chrome、Edge、Firefox、Safari などの最新のウェブブラウザは、ブラウザ市場の 90% 以上を占めています。また、同じ基盤となるレンダリング エンジンを使用する他のブラウザが 5% を占めています。つまり、世界のウェブ トラフィックの 95% は、過去 10 年間で最も広く使用されている JavaScript 言語機能をサポートするブラウザから発生しています。これには次のものがあります。

  • クラス(ES2015)
  • アロー関数(ES2015)
  • ジェネレータ(ES2015)
  • ブロック スコープ(ES2015)
  • 分解(ES2015)
  • rest パラメータと spread パラメータ(ES2015)
  • オブジェクトの省略形(ES2015)
  • async/await(ES2017)

一般に、新しいバージョンの言語仕様の機能は、最新のブラウザ間で一貫したサポートが提供されていません。たとえば、ES2020 と ES2021 の多くの機能は、ブラウザ市場の 70% でのみサポートされています。これはブラウザの大多数を占めますが、これらの機能を直接使用できるほど十分ではありません。つまり、「最新」の JavaScript は変化し続けるターゲットですが、ES2017 は、一般に使用されている最新の構文機能のほとんどを備えながら、ブラウザとの互換性が最も広範です。つまり、ES2017 は現在の最新の構文に最も近いのです。

従来の JavaScript

レガシー JavaScript は、上記のすべての言語機能を使用しないコードです。ほとんどのデベロッパーは最新の構文を使用してソースコードを記述しますが、ブラウザのサポートを強化するために、すべてを以前の構文にコンパイルします。以前の構文にコンパイルするとブラウザのサポートは増えますが、その効果は想定よりも小さいことがよくあります。多くの場合、サポートは 95% から 98% に向上しますが、大幅な費用が発生します。

  • 通常、レガシー JavaScript は同等の最新コードよりも約 20% 大きく、遅くなります。ツールの欠陥や構成ミスにより、このギャップはさらに広がることがよくあります。

  • インストールされたライブラリは、一般的な本番環境の JavaScript コードの 90% を占めています。ライブラリ コードでは、ポリフィルとヘルパーの重複により、従来の JavaScript のオーバーヘッドがさらに増加します。これは、最新のコードが公開されることで回避できます。

npm の最新の JavaScript

最近、Node.js では "exports" フィールドが標準化され、パッケージのエントランス ポイントを定義できるようになりました。

{
  "exports": "./index.js"
}

"exports" フィールドで参照されるモジュールは、ES2019 をサポートする Node バージョン 12.8 以降を前提としています。つまり、"exports" フィールドを使用して参照されるモジュールは、最新の JavaScript で記述できます。パッケージの使用者は、"exports" フィールドを持つモジュールに最新のコードが含まれていることを前提とし、必要に応じてトランスパイルする必要があります。

モダンのみ

最新のコードを含むパッケージを公開し、コンシューマが依存関係として使用するときにトランスパイルを処理できるようにする場合は、"exports" フィールドのみを使用します。

{
  "name": "foo",
  "exports": "./modern.js"
}

モダン(従来版のフォールバックあり)

最新のコードを使用してパッケージを公開し、以前のブラウザ向けに ES5 + CommonJS のフォールバックも含めるには、"main" とともに "exports" フィールドを使用します。

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

レガシー フォールバックと ESM バンドルツールの最適化によるモダナイゼーション

"module" フィールドは、代替の CommonJS エントリポイントを定義するだけでなく、JavaScript モジュール構文(importexport)を使用する同様のレガシー フォールバック バンドルを参照するためにも使用できます。

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

webpack や Rollup などの多くのバンドルは、このフィールドを使用してモジュール機能を活用し、ツリー シェイキングを有効にします。これは、import/export 構文以外に最新のコードが含まれていないレガシー バンドルです。この方法では、バンドル用に最適化されたレガシー フォールバックとともに最新のコードを配信します。

アプリケーションでの最新の JavaScript

サードパーティの依存関係は、ウェブ アプリケーションの一般的な本番環境 JavaScript コードの大部分を占めています。npm の依存関係は従来、従来の ES5 構文として公開されていましたが、これはもはや安全な前提ではなく、依存関係の更新によってアプリケーションのブラウザ サポートが中断されるリスクがあります。

npm パッケージが最新の JavaScript に移行するケースが増えているため、それらを処理するようにビルドツールが設定されていることを確認することが重要です。依存している npm パッケージの一部は、すでに最新の言語機能を使用している可能性があります。古いブラウザでアプリケーションを破壊することなく npm の最新のコードを使用するには、いくつかの方法があります。一般的な考え方は、ビルドシステムで依存関係をソースコードと同じ構文ターゲットにトランスパイルすることです。

webpack

webpack 5 では、バンドルとモジュールのコード生成時に webpack が使用する構文を構成できるようになりました。これにより、コードや依存関係がトランスパイルされることはありません。影響するのは、webpack によって生成された「グルー」コードのみです。ブラウザ サポート ターゲットを指定するには、プロジェクトに browserslist 構成を追加するか、webpack 構成で直接指定します。

module.exports = {
  target: ['web', 'es2017'],
};

最新の ES モジュール環境をターゲットとするときに、不要なラッパー関数を省略した最適化されたバンドルを生成するように webpack を構成することもできます。また、<script type="module"> を使用してコード分割バンドルを読み込むように webpack を構成します。

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Optimize Plugin や BabelEsmPlugin など、従来のブラウザをサポートしながら最新の JavaScript をコンパイルして出荷できる webpack プラグインがいくつかあります。

Optimize Plugin

Optimize Plugin は、個々のソースファイルではなく、最終的なバンドル コードを最新の JavaScript からレガシー JavaScript に変換する webpack プラグインです。これは自己完結型の設定で、webpack 構成ですべてが最新の JavaScript であると想定し、複数の出力や構文に特別な分岐を設定する必要はありません。

Optimize Plugin は個々のモジュールではなくバンドルで動作するため、アプリケーションのコードと依存関係を同等に処理します。これにより、npm の最新の JavaScript 依存関係を安全に使用できます。コードはバンドルされ、正しい構文にトランスパイルされるためです。また、2 つのコンパイル ステップを含む従来のソリューションよりも高速に処理できます。その場合でも、モダン ブラウザとレガシー ブラウザ用に個別のバンドルが生成されます。2 つのバンドルは、module/nomodule パターンを使用して読み込まれるように設計されています。

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin は、通常はモダン コードとレガシー コードを個別にバンドルするカスタム webpack 構成よりも高速で効率的です。また、Babel の実行も処理し、最新の出力とレガシー出力に別々の最適な設定を使用して Terser を使用してバンドルを圧縮します。最後に、生成されたレガシー バンドルに必要なポリフィルが専用のスクリプトに抽出されるため、新しいブラウザで重複したり、不必要に読み込まれたりすることはありません。

比較: ソース モジュールを 2 回変換する場合と、生成されたバンドルを変換する場合。

BabelEsmPlugin

BabelEsmPlugin は、@babel/preset-env と連携して既存のバンドルの最新バージョンを生成し、最新のブラウザにトランスパイルされたコードをより少なく出荷する webpack プラグインです。これは、module/nomodule 用の最も一般的な市販ソリューションであり、Next.jsPreact CLI で使用されています。

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin は、アプリケーションの 2 つのビルドをほぼ別々に実行するため、さまざまな webpack 構成をサポートしています。大規模なアプリケーションでは、2 回コンパイルすると少し時間がかかりますが、この手法では BabelEsmPlugin を既存の webpack 構成にシームレスに統合できるため、最も便利なオプションの 1 つになります。

node_modules をトランスパイルするように babel-loader を構成する

上記の 2 つのプラグインのいずれかを使用せずに babel-loader を使用している場合は、最新の JavaScript npm モジュールを使用するには重要な手順が必要です。2 つの個別の babel-loader 構成を定義すると、node_modules にある最新の言語機能を ES2017 に自動的にコンパイルしながら、プロジェクトの構成で定義された Babel プラグインとプリセットを使用して独自のファーストパーティ コードをトランスパイルできます。これにより、モジュールあり/モジュールなしの設定用の最新のバンドルと従来のバンドルは生成されませんが、古いブラウザを損なうことなく、最新の JavaScript を含む npm パッケージをインストールして使用できるようになります。

webpack-plugin-modern-npm は、この手法を使用して、package.json"exports" フィールドを持つ npm 依存関係をコンパイルします。これらの依存関係には、最新の構文が含まれている可能性があります。

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

また、モジュールの解決時にモジュールの package.json"exports" フィールドを確認することで、この手法を webpack 構成に手動で実装することもできます。簡潔にするため、キャッシュを省略したカスタム実装は次のようになります。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

この方法を使用する場合は、最新の構文が圧縮ツールでサポートされていることを確認する必要があります。Terseruglify-es の両方には、圧縮とフォーマット中に ES2017 構文を保持し、場合によっては生成するために {ecma: 2017} を指定するオプションがあります。

ロールアップ

ロールアップには、単一のビルドの一部として複数のバンドルセットを生成するためのサポートが組み込まれており、デフォルトで最新のコードが生成されます。そのため、Rollup は、すでに使用している公式プラグインを使用してモダン バンドルとレガシー バンドルを生成するように構成できます。

@rollup/plugin-babel

Rollup を使用する場合、getBabelOutputPlugin() メソッド(Rollup の公式 Babel プラグインによって提供)は、個々のソース モジュールではなく、生成されたバンドルのコードを変換します。Rollup には、1 つのビルドの一部として複数のバンドルセットを生成するためのサポートが組み込まれています。各バンドルセットには独自のプラグインがあります。これを使用して、それぞれを異なる Babel 出力プラグイン構成に渡すことで、モダンとレガシーの異なるバンドルを生成できます。

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

その他のビルドツール

Rollup と webpack は高度に構成可能です。通常、各プロジェクトは、依存関係で最新の JavaScript 構文を有効にするために構成を更新する必要があります。ParcelSnowpackViteWMR など、構成よりも規則とデフォルトを重視する上位レベルのビルドツールもあります。これらのツールのほとんどは、npm の依存関係に最新の構文が含まれていることを前提としており、本番環境用にビルドするときに適切な構文レベルにトランスパイルします。

webpack と Rollup 専用のプラグインに加えて、レガシー フォールバックを含む最新の JavaScript バンドルを、デビオレーションを使用して任意のプロジェクトに追加できます。Devolution は、ビルドシステムの出力を変換してレガシー JavaScript バリアントを生成するスタンドアロン ツールです。これにより、バンドルと変換で最新の出力ターゲットを想定できます。