Publiez, livrez et installez du code JavaScript moderne pour des applications plus rapides

Améliorez les performances en activant les dépendances et la sortie JavaScript modernes.

Plus de 90% des navigateurs sont capables d'exécuter du code JavaScript moderne, mais la prévalence de l'ancien JavaScript demeure une importante source de problèmes de performances sur le Web à l'heure actuelle.

JavaScript moderne

Le JavaScript moderne n'est pas caractérisé comme du code écrit dans une version spécifique de la spécification ECMAScript, mais plutôt dans une syntaxe compatible avec tous les navigateurs modernes. Les navigateurs Web modernes tels que Chrome, Edge, Firefox et Safari représentent plus de 90 % du marché des navigateurs, et les différents navigateurs qui reposent sur les mêmes moteurs de rendu sous-jacents représentent 5 % supplémentaires. Cela signifie que 95 % du trafic Web mondial provient de navigateurs compatibles avec les fonctionnalités de langage JavaScript les plus utilisées au cours des 10 dernières années, y compris :

  • Classes (ES2015)
  • Fonctions Arrow (ES2015)
  • Générateurs (ES2015)
  • Étendue des blocs (ES2015)
  • Déstructuration (ES2015)
  • Paramètres de repos et de répartition (ES2015)
  • Abréviation d'objet (ES2015)
  • Async/await (ES2017)

Les fonctionnalités des versions plus récentes de la spécification de la langue sont généralement moins compatibles avec les navigateurs modernes. Par exemple, de nombreuses fonctionnalités ES2020 et ES2021 ne sont compatibles qu'avec 70% du marché des navigateurs. C'est encore la majorité des navigateurs, mais ce n'est pas suffisant pour qu'il soit possible d'utiliser directement ces fonctionnalités en toute sécurité. Cela signifie que, même si le JavaScript "moderne" est une cible mouvante, ES2017 est compatible avec le plus large éventail de navigateurs, tout en incluant la plupart des fonctionnalités de syntaxe modernes couramment utilisées. En d'autres termes, ES2017 est la syntaxe la plus proche de la syntaxe moderne à ce jour.

Ancien JavaScript

L'ancien code JavaScript est un code qui évite spécifiquement d'utiliser toutes les fonctionnalités de langage ci-dessus. La plupart des développeurs écrivent leur code source à l'aide d'une syntaxe moderne, mais compilent tout en syntaxe héritée pour une compatibilité accrue avec les navigateurs. La compilation en syntaxe ancienne augmente la compatibilité avec les navigateurs, mais l'effet est souvent moins important que nous le pensons. Dans de nombreux cas, le taux d'assistance passe de 95 % à 98 %, ce qui entraîne des coûts importants :

  • Le code JavaScript ancien est généralement environ 20 % plus volumineux et plus lent que le code moderne équivalent. Les défauts d'outils et les erreurs de configuration creusent souvent encore davantage cet écart.

  • Les bibliothèques installées représentent jusqu'à 90% du code JavaScript de production standard. Le code de la bibliothèque entraîne des coûts encore plus élevés en JavaScript ancien en raison de la duplication de polyfills et d'outils d'assistance qui pourrait être évitée en publiant du code moderne.

Modern JavaScript sur npm

Récemment, Node.js a standardisé un champ "exports" pour définir les points d'entrée d'un package :

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

Les modules référencés par le champ "exports" impliquent une version de nœud d'au moins 12.8, compatible avec ES2019. Cela signifie que n'importe quel module référencé à l'aide du champ "exports" peut être écrit en JavaScript moderne. Les clients de packages doivent supposer que les modules avec un champ "exports" contiennent du code moderne et sont transpilés si nécessaire.

Moderne uniquement

Si vous souhaitez publier un package avec du code moderne et laisser au client le soin de le transpiler lorsqu'il l'utilise comme dépendance, utilisez uniquement le champ "exports".

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

Moderne avec ancien paramètre de remplacement

Utilisez le champ "exports" avec "main" pour publier votre package à l'aide de code moderne, mais aussi pour inclure un remplacement ES5 + CommonJS pour les anciens navigateurs.

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

Moderne avec ancien mode de remplacement et optimisations du bundler ESM

En plus de définir un point d'entrée CommonJS de remplacement, le champ "module" peut être utilisé pour pointer vers un ancien bundle de remplacement similaire, mais qui utilise la syntaxe de module JavaScript (import et export).

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

De nombreux outils de compilation, tels que webpack et Rollup, s'appuient sur ce champ pour exploiter les fonctionnalités des modules et activer le tree shaking. Il s'agit toujours d'un ancien bundle qui ne contient aucun code moderne en dehors de la syntaxe import/export. Utilisez donc cette approche pour distribuer du code moderne avec un ancien fallback qui est toujours optimisé pour le regroupement.

Modern JavaScript dans les applications

Les dépendances tierces représentent la grande majorité du code JavaScript de production typique dans les applications Web. Bien que les dépendances npm aient été historiquement publiées sous la forme d'une ancienne syntaxe ES5, cette hypothèse n'est plus sûre et risque d'entraîner des mises à jour de dépendances susceptibles de perturber la compatibilité des navigateurs dans votre application.

De plus en plus de packages npm passent au JavaScript moderne. Il est donc important de s'assurer que les outils de compilation sont configurés pour les gérer. Il y a de fortes chances que certains des packages npm dont vous dépendez utilisent déjà des fonctionnalités de langage moderne. Il existe un certain nombre d'options pour utiliser le code moderne de npm sans endommager votre application dans les anciens navigateurs, mais l'idée générale est de demander au système de compilation de transcompiler les dépendances vers la même cible de syntaxe que votre code source.

Webpack

Depuis webpack 5, il est désormais possible de configurer la syntaxe que webpack utilisera lors de la génération de code pour les bundles et les modules. Cela ne transpile pas votre code ni vos dépendances, mais uniquement le code de liaison généré par webpack. Pour spécifier la cible de compatibilité du navigateur, ajoutez une configuration browserslist à votre projet ou effectuez-le directement dans votre configuration webpack :

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

Il est également possible de configurer webpack pour générer des bundles optimisés qui omettent les fonctions de wrapper inutiles lorsque vous ciblez un environnement de modules ES moderne. Cela configure également webpack pour charger des bundles de fractionnement de code à l'aide de <script type="module">.

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

Plusieurs plug-ins webpack sont disponibles pour compiler et distribuer du code JavaScript moderne tout en prenant en charge les anciens navigateurs, tels que Optimize Plugin et BabelEsmPlugin.

Plug-in Optimize

Le plug-in Optimize est un plug-in webpack qui transforme le code groupé final du code JavaScript moderne vers l'ancien JavaScript au lieu de chaque fichier source individuel. Il s'agit d'une configuration autonome qui permet à votre configuration webpack de supposer que tout est du JavaScript moderne, sans ramification spéciale pour plusieurs sorties ou syntaxes.

Étant donné que le plug-in d'optimisation fonctionne sur des bundles plutôt que sur des modules individuels, il traite le code de votre application et vos dépendances de manière égale. Vous pouvez ainsi utiliser des dépendances JavaScript modernes à partir de npm en toute sécurité, car leur code sera regroupé et transcompilé dans la syntaxe appropriée. Il peut également être plus rapide que les solutions traditionnelles impliquant deux étapes de compilation, tout en générant des bundles distincts pour les navigateurs modernes et anciens. Les deux ensembles d'app bundles sont conçus pour être chargés à l'aide du modèle module/nomodule.

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

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

Optimize Plugin peut être plus rapide et plus efficace que les configurations webpack personnalisées, qui regroupent généralement le code moderne et ancien séparément. Il gère également l'exécution de Babel à votre place et réduit la taille des lots à l'aide de Terser avec des paramètres optimaux distincts pour les sorties modernes et anciennes. Enfin, les polyfills nécessaires aux anciens bundles générés sont extraits dans un script dédié afin qu'ils ne soient jamais dupliqués ni chargés inutilement dans les navigateurs plus récents.

Comparaison : transcompilation des modules sources deux fois par rapport à la transcompilation des bundles générés.

BabelEsmPlugin

BabelEsmPlugin est un plug-in webpack qui fonctionne avec @babel/preset-env pour générer des versions modernes de bundles existants afin de transmettre du code moins transcompilé aux navigateurs récents. Il s'agit de la solution prête à l'emploi la plus populaire pour module/nomodule, utilisée par Next.js et la CLI Preact.

// 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 est compatible avec un large éventail de configurations webpack, car il exécute deux builds largement distincts de votre application. La compilation deux fois peut prendre un peu plus de temps pour les applications volumineuses. Toutefois, cette technique permet à BabelEsmPlugin de s'intégrer parfaitement aux configurations webpack existantes et en fait l'une des options les plus pratiques disponibles.

Configurer babel-loader pour transcompiler node_modules

Si vous utilisez babel-loader sans l'un des deux plugins précédents, une étape importante est requise pour consommer des modules npm JavaScript modernes. Définir deux configurations babel-loader distinctes permet de compiler automatiquement les fonctionnalités de langage modernes trouvées dans node_modules en ES2017, tout en transpilant votre propre code propriétaire avec les plug-ins et les préréglages Babel définis dans la configuration de votre projet. Cela ne génère pas de bundles modernes et anciens pour une configuration de module/nomodule, mais il permet d'installer et d'utiliser des packages npm contenant du JavaScript moderne sans interrompre les anciens navigateurs.

webpack-plugin-modern-npm utilise cette technique pour compiler les dépendances npm qui comportent un champ "exports" dans leur package.json, car elles peuvent contenir une syntaxe moderne :

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

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

Vous pouvez également implémenter la technique manuellement dans votre configuration webpack en recherchant un champ "exports" dans le package.json des modules au fur et à mesure de leur résolution. Si vous omettez la mise en cache par souci de concision, une implémentation personnalisée peut se présenter comme suit:

// 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'],
          },
        },
      },
    ],
  },
};

Lorsque vous utilisez cette approche, vous devez vous assurer que votre outil de minification est compatible avec la syntaxe moderne. Terser et uglify-es proposent tous deux une option permettant de spécifier {ecma: 2017} afin de préserver et, dans certains cas, de générer la syntaxe ES2017 lors de la compression et de la mise en forme.

Résumé

Le regroupement est compatible avec la génération de plusieurs ensembles de bundles dans le cadre d'une seule compilation et génère du code moderne par défaut. Par conséquent, Rollup peut être configuré pour générer des bundles modernes et anciens avec les plug-ins officiels que vous utilisez probablement déjà.

@rollup/plugin-babel

Si vous utilisez Rollup, la méthode getBabelOutputPlugin() (fournie par le plug-in Babel officiel de Rollup) transforme le code en bundles générés plutôt que dans des modules sources individuels. La propriété de consolidation permet de générer plusieurs ensembles de bundles dans un seul build, chacun avec ses propres plug-ins. Vous pouvez l'utiliser pour produire différents bundles pour les styles modernes et anciens, en les transmettant via une configuration différente du plug-in de sortie 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'],
        }),
      ],
    },
  ],
};

Autres outils de compilation

Rollup et Webpack sont hautement configurables, ce qui signifie généralement que chaque projet doit mettre à jour sa configuration pour activer la syntaxe JavaScript moderne dans les dépendances. Il existe également des outils de compilation de niveau supérieur qui privilégient les conventions et les valeurs par défaut par rapport à la configuration, comme Parcel, Snowpack, Vite et WMR. La plupart de ces outils supposent que les dépendances npm peuvent contenir une syntaxe moderne et les transpilent au ou aux niveaux de syntaxe appropriés lors de la compilation pour la production.

En plus des plug-ins dédiés à webpack et Rollup, des bundles JavaScript modernes avec des anciens fallbacks peuvent être ajoutés à n'importe quel projet à l'aide de la dévolution. Devolution est un outil autonome qui transforme la sortie d'un système de compilation pour produire des variantes JavaScript obsolètes, ce qui permet au regroupement et aux transformations de supposer une cible de sortie moderne.