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

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

90% を超えるブラウザで最新の JavaScript を実行できますが、従来の JavaScript の普及は、現在、ウェブのパフォーマンスに関する大きな問題の原因となっています。

最新の JavaScript

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

  • クラス(ES2015)
  • アロー関数(ES2015)
  • 発電機(ES2015)
  • ブロック スコープ(ES2015)
  • 分解(ES2015)
  • REST とスプレッドのパラメータ(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 をサポートする 12.8 以上の Node バージョンを意味します。つまり、"exports" フィールドを使用して参照されるモジュールは、最新の JavaScript で記述できます。パッケージ コンシューマは、"exports" フィールドを持つモジュールに最新のコードが含まれていると想定し、必要に応じてトランスパイルする必要があります。

モダンのみ

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

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

最新(従来のフォールバック)

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

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

従来のフォールバックと ESM バンドラの最適化を備えた最新型

フォールバックの CommonJS エントリポイントを定義するだけでなく、"module" フィールドを使用して、同様の以前のフォールバック バンドルをポイントすることもできますが、バンドルは 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 は、個々のソースファイルではなく、最終的なバンドルコードを最新の 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 プラグインです。これにより、トランスパイルされていないコードを最新のブラウザに配布できます。これは、Next.jsPreact CLI で使用される module/nomodule の最も一般的な既製のソリューションです。

// 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 構成にシームレスに統合でき、最も便利なオプションの一つになっています。

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

前述の 2 つのプラグインのいずれかなしで babel-loader を使用している場合は、最新の JavaScript npm モジュールを使用するために重要な手順があります。2 つの個別の babel-loader 構成を定義すると、node_modules にある最新の言語機能を ES2017 に自動的にコンパイルすると同時に、プロジェクトの構成で定義された Babel プラグインとプリセットを使用して独自のファースト パーティ コードをトランスパイルできます。これにより、モジュール/nomodule 設定用の最新のバンドルと以前のバンドルは生成されませんが、古いブラウザを破壊することなく、最新の 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 には、1 つのビルドの一部として複数のバンドルセットを生成するためのサポートが組み込まれており、デフォルトで最新のコードを生成します。そのため、すでに使用している公式プラグインで最新のバンドルと従来のバンドルを生成するように 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 を使用して任意のプロジェクトに追加できます。Devolution は、ビルドシステムからの出力を変換して以前の JavaScript バリアントを生成するスタンドアロン ツールです。これにより、バンドルと変換で最新の出力ターゲットを想定できます。