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

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

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

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

  • クラス(ES2015)
  • Arrow 関数(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 バンドルツールの最適化によるモダナイゼーション

フォールバックの CommonJS エントリポイントを定義するだけでなく、"module" フィールドを使用して同様の以前のフォールバック バンドルを指定することもできますが、1 つは JavaScript モジュール構文(importexport)を使用します。

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

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

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

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

最新の JavaScript に移行する npm パッケージの数が増えるなか、ビルドツールがそれらのパッケージを処理するように設定されていることを確認することが重要です。依存している 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 であると想定し、複数の出力や構文に特別な分岐を設定する必要はありません。

オプティマイズ プラグインは、個々のモジュールではなくバンドルで動作するため、アプリケーションのコードと依存関係を同等に処理します。これにより、コードがバンドルされて正しい構文にトランスパイルされるため、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 バリアントを生成するスタンドアロン ツールです。これにより、バンドルと変換で最新の出力ターゲットを想定できます。