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

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

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

JavaScript moderne

Le JavaScript moderne n'est pas considéré comme du code écrit dans une version de spécification ECMAScript spécifique, mais plutôt comme 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. Les autres navigateurs, qui s'appuient sur les mêmes moteurs de rendu sous-jacents, représentent 5 % de plus. Cela signifie que 95% du trafic Web mondial provient de navigateurs compatibles avec les fonctionnalités du langage JavaScript les plus utilisées au cours des 10 dernières années, y compris:

  • Classes (ES2015)
  • Fonctions fléchées (ES2015)
  • Générateurs (ES2015)
  • Portée des blocs (ES2015)
  • Déstructuration (ES2015)
  • Paramètres de repos et d'écart (ES2015)
  • Raccourci des objets (ES2015)
  • Async/await (ES2017)

Les fonctionnalités des versions les plus récentes de la spécification de langue sont généralement moins compatibles avec les navigateurs récents. Par exemple, de nombreuses fonctionnalités ES2020 et ES2021 ne sont compatibles qu'avec 70% du marché des navigateurs (ce qui reste pour la majorité des navigateurs), mais ce n'est pas suffisant pour que vous puissiez vous appuyer directement sur ces fonctionnalités en toute sécurité. Cela signifie que bien que le code JavaScript "moderne" soit une cible mouvante, ES2017 offre la plus large gamme de compatibilités 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 aujourd'hui.

Ancien JavaScript

L'ancien JavaScript est un code qui évite spécifiquement l'utilisation de toutes les fonctionnalités du langage ci-dessus. La plupart des développeurs écrivent leur code source à l'aide d'une syntaxe moderne, mais compilent tout dans une ancienne syntaxe pour une meilleure compatibilité avec les navigateurs. La compilation avec l'ancienne syntaxe permet d'améliorer la compatibilité avec les navigateurs, mais l'effet est souvent moins important que nous ne le réalisons. Dans de nombreux cas, l'assistance passe d'environ 95 % à 98 %, mais les coûts sont importants:

  • L'ancien JavaScript est généralement 20% plus volumineux et plus lent que le code moderne équivalent. Les lacunes et les erreurs de configuration des outils ne font souvent qu'aggraver ce fossé.

  • Les bibliothèques installées représentent jusqu'à 90% du code JavaScript de production standard. Le code de bibliothèque entraîne des frais généraux liés à l'ancien JavaScript encore plus élevés en raison des polyfills et de la duplication d'aide, qui pourraient être évités en publiant du code moderne.

Code JavaScript moderne 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 tout 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 peuvent être transpilés si nécessaire.

Moderne uniquement

Si vous souhaitez publier un package avec du code moderne et laisser le consommateur le faire pour le transpiler lorsqu'il l'utilise en tant que dépendance, utilisez uniquement le champ "exports".

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

Moderne avec anciennes créations de remplacement

Utilisez le champ "exports" avec "main" pour publier votre package à l'aide d'un code moderne, mais incluez également une solution de secours ES5 + CommonJS pour les anciens navigateurs.

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

Modernisation avec les anciens modèles de remplacement et les optimisations de 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 bundlers, 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 hormis la syntaxe import/export. Utilisez cette approche pour envoyer du code moderne avec un ancien remplacement qui est toujours optimisé pour le regroupement.

Code JavaScript moderne dans les applications

Les dépendances tierces constituent la grande majorité du code JavaScript de production classique dans les applications Web. Bien que les dépendances npm aient toujours été publiées sous la forme d'une ancienne syntaxe ES5, cette hypothèse n'est plus sûre et les mises à jour des dépendances risquent de rompre la compatibilité des navigateurs avec votre application.

Étant donné que de plus en plus de packages npm passent au code JavaScript moderne, il est important de s'assurer que les outils de compilation sont configurés pour les gérer. Il est fort probable 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 permettant d'utiliser le code moderne de npm sans interrompre votre application dans des navigateurs plus anciens, mais l'idée générale est que le système de compilation transpile les dépendances vers la même syntaxe cible que votre code source.

pack Web

À partir de la version 5, il est possible de configurer la syntaxe qu'il utilisera lors de la génération de code pour des bundles et des modules. Cela ne transpile pas votre code ni vos dépendances. Seul le code "glue" généré par webpack est affecté. Pour spécifier la cible de compatibilité du navigateur, ajoutez une configuration de type "browserslist" à votre projet ou faites-la 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 groupes de divisions de code à l'aide de <script type="module">.

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

Il existe un certain nombre de plug-ins webpack disponibles qui permettent de compiler et de distribuer du code JavaScript moderne tout en prenant en charge les anciens navigateurs, tels que le plug-in Optimize et BabelEsmPlugin.

Plug-in Optimize

Le plug-in Optimize est un plug-in webpack qui transforme le code final groupé en 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 un code JavaScript moderne sans ramification spéciale pour plusieurs sorties ou syntaxes.

Étant donné que le plug-in Optimize fonctionne sur des bundles et non sur des modules individuels, il traite le code de votre application et vos dépendances de manière égale. Il est ainsi plus sécurisé d'utiliser des dépendances JavaScript modernes de npm, car leur code sera regroupé et transpilé dans la syntaxe correcte. Elle 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 les anciens. Les deux ensembles de 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 efficace que les configurations Webpack personnalisées, qui regroupent généralement le code moderne et l'ancien code séparément. Il gère également l'exécution de Babel et minimise les bundles à l'aide de Terser avec des paramètres optimaux distincts pour les sorties modernes et les anciennes. Enfin, les polyfills nécessaires aux anciens bundles générés sont extraits dans un script dédié. Ils ne sont donc jamais dupliqués ni chargés inutilement dans des navigateurs plus récents.

Comparaison: transpilation des modules sources deux fois et transpilation de 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 d'envoyer du code moins transpilé aux navigateurs modernes. Il s'agit de la solution prête à l'emploi la plus populaire pour les modules/nomodules, utilisée par Next.js et Preact 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 est compatible avec un large éventail de configurations Webpack, car il exécute deux versions largement distinctes de votre application. Compilation deux fois peut prendre un peu plus de temps pour les applications volumineuses, mais 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 transpiler les node_modules

Si vous utilisez babel-loader sans l'un des deux plug-ins précédents, une étape importante est nécessaire pour utiliser les modules npm JavaScript modernes. La définition de deux configurations babel-loader distinctes permet de compiler automatiquement les fonctionnalités de langage modernes disponibles dans node_modules à ES2017, tout en continuant de transpiler votre propre code propriétaire avec les plug-ins et préréglages Babel définis dans la configuration de votre projet. Cette opération ne génère pas de bundles modernes et anciens pour une configuration de module/nomodule, mais elle permet d'installer et d'utiliser des packages npm contenant du code JavaScript moderne sans interrompre les anciens navigateurs.

webpack-plugin-modern-npm utilise cette technique pour compiler les dépendances npm dont le package.json comporte un champ "exports", car celles-ci 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 la configuration de votre pack Web en recherchant un champ "exports" dans le package.json des modules au fur et à mesure qu'ils sont résolus. En omettant la mise en cache pour des raisons 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 la syntaxe moderne est prise en charge par votre outil de minimisation. Terser et uglify-es ont tous deux la possibilité de spécifier {ecma: 2017} afin de conserver et, dans certains cas, de générer la syntaxe ES2017 lors de la compression et du formatage.

Regrouper

Le rattachement permet de générer plusieurs ensembles de groupes dans un même build et génère du code moderne par défaut. Par conséquent, le rattachement 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 la propriété de consolidation, 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. Le rattachement permet de générer plusieurs ensembles de groupes dans une même compilation, chacun avec ses propres plug-ins. Vous pouvez l'utiliser afin de produire différents bundles pour les styles moderne et ancien en transmettant chacun d'eux 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'],
        }),
      ],
    },
  ],
};

Outils de compilation supplémentaires

Le Rollup et le 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, tels que Parcel, Snowpack, Vite et WMR. La plupart de ces outils supposent que les dépendances npm peuvent contenir une syntaxe moderne, et les transpilent vers les niveaux de syntaxe appropriés lors de la compilation pour la production.

En plus des plug-ins dédiés pour webpack et Rollup, des bundles JavaScript modernes avec d'anciennes solutions de remplacement peuvent être ajoutés à n'importe quel projet grâce à la devolution. Devolution est un outil autonome qui transforme la sortie d'un système de compilation pour produire d'anciennes variantes JavaScript, permettant le regroupement et les transformations pour supposer une cible de sortie moderne.