Intégrez un code moderne dans les navigateurs récents pour accélérer le chargement des pages.

Dans cet atelier de programmation, vous allez améliorer les performances de cette application simple qui permet aux utilisateurs d'évaluer des chats au hasard. Découvrez comment optimiser le bundle JavaScript en minimisant la quantité de code transpilée.

Capture d'écran de l'application

Dans l'application exemple, vous pouvez sélectionner un mot ou un emoji pour indiquer à quel point vous aimez chaque chat. Lorsque vous cliquez sur un bouton, l'application affiche la valeur du bouton sous l'image de chat actuelle.

Mesurer

Il est toujours judicieux de commencer par inspecter un site Web avant d'ajouter une optimisation:

  1. Pour prévisualiser le site, appuyez sur Afficher l'application, puis sur Plein écran plein écran.
  2. Appuyez sur Ctrl+Maj+J (ou Cmd+Option+J sur Mac) pour ouvrir les outils de développement.
  3. Cliquez sur l'onglet Réseau.
  4. Cochez la case Désactiver le cache.
  5. Actualisez l'application.

Demande de taille de lot d'origine

Cette application utilise plus de 80 Ko. Vérifions si certaines parties du bundle ne sont pas utilisées:

  1. Appuyez sur Control+Shift+P (ou Command+Shift+P sur Mac) pour ouvrir le menu Command (Commande). Menu de commandes

  2. Saisissez Show Coverage, puis appuyez sur Enter pour afficher l'onglet Couverture.

  3. Dans l'onglet Couverture, cliquez sur Actualiser pour actualiser l'application tout en capturant la couverture.

    Actualiser l'application avec une couverture de code

  4. Comparez la quantité de code utilisée et la quantité chargée pour le bundle principal:

    Couverture de code du bundle

Plus de la moitié du bundle (44 Ko) n'est même pas utilisé. En effet, une grande partie du code se compose de polyfills pour garantir le bon fonctionnement de l'application dans les navigateurs plus anciens.

Utiliser @babel/preset-env

La syntaxe du langage JavaScript est conforme à une norme appelée ECMAScript, ou ECMA-262. De nouvelles versions de la spécification sont publiées chaque année et incluent de nouvelles fonctionnalités ayant passé avec succès le processus de proposition. Chaque navigateur principal est toujours à un stade différent de la prise en charge de ces fonctionnalités.

Les fonctionnalités ES2015 suivantes sont utilisées dans l'application:

La fonctionnalité ES2017 suivante est également utilisée:

N'hésitez pas à examiner le code source dans src/index.js pour voir comment tout cela est utilisé.

Toutes ces fonctionnalités sont compatibles avec la dernière version de Chrome, mais qu'en est-il des autres navigateurs qui ne sont pas compatibles ? Babel, qui est inclus dans l'application, est la bibliothèque la plus populaire utilisée pour compiler du code contenant une syntaxe plus récente que les navigateurs et environnements plus anciens. Pour cela, il procède de deux manières:

  • Des polyfills sont inclus pour émuler les fonctions ES2015+ les plus récentes afin que leurs API puissent être utilisées même si elles ne sont pas compatibles avec le navigateur. Voici un exemple de polyfill de la méthode Array.includes.
  • Les plug-ins permettent de transformer le code ES2015 (ou version ultérieure) en une ancienne syntaxe ES5. Étant donné qu'il s'agit de modifications liées à la syntaxe (telles que les fonctions fléchées), elles ne peuvent pas être émulées avec des polyfills.

Examinez package.json pour savoir quelles bibliothèques Babel sont incluses:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core est le compilateur Babel principal. Toutes les configurations Babel sont alors définies dans un .babelrc à la racine du projet.
  • babel-loader inclut Babel dans le processus de compilation du pack Web.

Examinez maintenant webpack.config.js pour voir comment babel-loader est inclus en tant que règle:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill fournit tous les polyfills nécessaires pour toutes les fonctionnalités ECMAScript plus récentes afin qu'elles puissent fonctionner dans les environnements qui ne les acceptent pas. Il est déjà importé tout en haut de src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env identifie les transformations et les polyfills nécessaires pour tous les navigateurs ou environnements choisis comme cibles.

Examinez le fichier de configuration Babel, .babelrc, pour découvrir comment il est inclus:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Il s'agit d'une configuration Babel et webpack. Découvrez comment inclure Babel dans votre application si vous utilisez un bundler de module différent de Webpack.

L'attribut targets dans .babelrc identifie les navigateurs ciblés. @babel/preset-env s'intègre à la liste des navigateurs. Vous trouverez donc la liste complète des requêtes compatibles pouvant être utilisées dans ce champ dans la documentation sur les listes de navigateurs.

La valeur "last 2 versions" transpile le code de l'application pour les deux dernières versions de chaque navigateur.

Débogage

Pour obtenir un aperçu complet de toutes les cibles Babel du navigateur, ainsi que de l'ensemble des transformations et polyfills inclus, ajoutez un champ debug à .babelrc:.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Cliquez sur Outils.
  • Cliquez sur Journaux.

Actualisez l'application et consultez les journaux d'état de Glitch en bas de l'éditeur.

Navigateurs ciblés

Babel enregistre un certain nombre d'informations dans la console concernant le processus de compilation, y compris tous les environnements cibles pour lesquels le code a été compilé.

Navigateurs ciblés

Notez que les navigateurs abandonnés, tels qu'Internet Explorer, sont inclus dans cette liste. Cela pose problème, car aucune fonctionnalité plus récente n'est ajoutée aux navigateurs non compatibles et Babel continue de transpiler une syntaxe spécifique. Cela augmente inutilement la taille de votre groupe si les utilisateurs n'utilisent pas ce navigateur pour accéder à votre site.

Babel enregistre également une liste des plug-ins de transformation utilisés:

Liste des plug-ins utilisés

C'est une longue liste ! Voici tous les plug-ins que Babel doit utiliser pour transformer une syntaxe ES2015+ en une ancienne syntaxe pour tous les navigateurs ciblés.

Toutefois, Babel ne montre pas de polyfills spécifiques utilisés:

Aucun polyfill ajouté

En effet, l'intégralité du fichier @babel/polyfill est importée directement.

Charger les polyfills individuellement

Par défaut, Babel inclut tous les polyfills nécessaires à un environnement ES2015+ complet lorsque @babel/polyfill est importé dans un fichier. Pour importer des polyfills spécifiques nécessaires pour les navigateurs cibles, ajoutez un useBuiltIns: 'entry' à la configuration.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Actualisez l'application. Vous pouvez maintenant voir tous les polyfills spécifiques inclus:

Liste des polyfills importés

Bien que seuls les polyfills nécessaires pour "last 2 versions" soient désormais inclus, la liste reste très longue. En effet, les polyfills nécessaires aux navigateurs cibles pour chaque fonctionnalité récente sont toujours inclus. Remplacez la valeur de l'attribut par usage pour n'inclure que celles nécessaires aux fonctionnalités utilisées dans le code.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Ainsi, des polyfills sont automatiquement inclus si nécessaire. Cela signifie que vous pouvez supprimer l'importation @babel/polyfill dans src/index.js.

import "./style.css";
import "@babel/polyfill";

Désormais, seuls les polyfills nécessaires à l'application sont inclus.

Liste des polyfills automatiquement incluse

La taille de l'app bundle est considérablement réduite.

Taille du bundle réduite à 30,1 Ko

Affiner la liste des navigateurs compatibles

Le nombre de navigateurs ciblés est encore assez important et peu d'utilisateurs utilisent des navigateurs abandonnés tels qu'Internet Explorer. Mettez à jour les configurations comme suit:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Examinez les détails du bundle récupéré.

Taille du bundle de 30 Ko

Étant donné que l'application est si petite, il n'y a pas vraiment de différence entre ces modifications. Toutefois, nous vous recommandons d'utiliser un pourcentage de part de marché des navigateurs (par exemple, ">0.25%") et d'exclure les navigateurs spécifiques que vous êtes certain que vos utilisateurs n'utilisent pas. Pour en savoir plus, consultez l'article Les deux dernières versions considérées comme dangereuses de James Kyle.

Utilisez <script type="module">

Il y a encore matière à amélioration. Bien qu'un certain nombre de polyfills inutilisés aient été supprimés, nombreux sont ceux qui sont en cours de distribution et qui ne sont pas nécessaires pour certains navigateurs. En utilisant des modules, une syntaxe plus récente peut être écrite et envoyée directement aux navigateurs sans utiliser de polyfills inutiles.

Les modules JavaScript sont une fonctionnalité relativement récente, compatible avec tous les principaux navigateurs. Les modules peuvent être créés à l'aide d'un attribut type="module" pour définir des scripts qui importent et exportent à partir d'autres modules. Exemple :

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

De nombreuses fonctionnalités ECMAScript plus récentes sont déjà prises en charge dans les environnements compatibles avec les modules JavaScript (au lieu d'avoir besoin de Babel). Cela signifie que la configuration de Babel peut être modifiée pour envoyer deux versions différentes de votre application au navigateur:

  • Version compatible avec les navigateurs plus récents compatibles avec les modules et comprenant un module non transpilé, mais dont la taille de fichier est inférieure
  • Une version qui comprend un script plus volumineux et transpilé qui fonctionnerait dans n'importe quel ancien navigateur

Utiliser les modules ES avec Babel

Pour utiliser des paramètres @babel/preset-env distincts pour les deux versions de l'application, supprimez le fichier .babelrc. Vous pouvez ajouter des paramètres Babel à la configuration du Webpack en spécifiant deux formats de compilation différents pour chaque version de l'application.

Commencez par ajouter une configuration pour l'ancien script à webpack.config.js:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Notez qu'au lieu d'utiliser la valeur targets pour "@babel/preset-env", on utilise esmodules avec la valeur false. Cela signifie que Babel inclut l'ensemble des transformations et polyfills nécessaires pour cibler tous les navigateurs qui ne sont pas encore compatibles avec les modules ES.

Ajoutez les objets entry, cssRule et corePlugins au début du fichier webpack.config.js. Ils sont tous partagés entre le module et les anciens scripts diffusés dans le navigateur.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

De même, créez un objet "config" pour le script de module ci-dessous où legacyConfig est défini:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

La principale différence réside dans le fait qu'une extension de fichier .mjs est utilisée pour le nom du fichier de sortie. La valeur esmodules est définie sur "true" ici, ce qui signifie que le code généré dans ce module est un script plus petit et moins compilé, qui ne subit aucune transformation dans cet exemple, car toutes les fonctionnalités utilisées sont déjà compatibles avec les navigateurs compatibles avec les modules.

À la fin du fichier, exportez les deux configurations dans un seul tableau.

module.exports = [
  legacyConfig, moduleConfig
];

Vous créez ainsi un module plus petit pour les navigateurs compatibles et un script transpilé plus volumineux pour les navigateurs plus anciens.

Les navigateurs compatibles avec les modules ignorent les scripts avec un attribut nomodule. À l'inverse, les navigateurs qui ne sont pas compatibles avec les modules ignorent les éléments de script avec type="module". Cela signifie que vous pouvez inclure un module ainsi qu'une compilation de remplacement compilée. Idéalement, les deux versions de l'application devraient être dans index.html comme suit:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Les navigateurs qui prennent en charge les modules récupèrent et exécutent main.mjs et ignorent main.bundle.js.. Les navigateurs qui ne prennent pas en charge les modules font le contraire.

Il est important de noter que, contrairement aux scripts classiques, les scripts de module sont toujours différés par défaut. Si vous souhaitez également que le script nomodule équivalent soit également différé et qu'il ne s'exécute qu'après l'analyse, vous devez ajouter l'attribut defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

La dernière étape consiste à ajouter les attributs module et nomodule respectivement au module et à l'ancien script, puis importez le plug-in ScriptExtHtmlWebpackPlugin tout en haut du fichier webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

À présent, mettez à jour le tableau plugins dans les configurations pour inclure ce plug-in:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

Ces paramètres de plug-in ajoutent un attribut type="module" pour tous les éléments de script .mjs ainsi qu'un attribut nomodule pour tous les modules de script .js.

Modules de diffusion dans le document HTML

La dernière étape consiste à renvoyer les éléments de script anciens et modernes dans le fichier HTML. Malheureusement, le plug-in qui crée le fichier HTML final, HTMLWebpackPlugin, n'est actuellement pas compatible avec la sortie des scripts module et nomodule. Bien qu'il existe des solutions de contournement et des plug-ins distincts pour résoudre ce problème, tels que BabelMultiTargetPlugin et HTMLWebpackMultiBuildPlugin, une approche plus simple permettant d'ajouter manuellement l'élément de script du module est utilisée pour ce tutoriel.

Ajoutez le code suivant à src/index.js à la fin du fichier:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Chargez maintenant l'application dans un navigateur compatible avec les modules, tel que la dernière version de Chrome.

Module de 5,2 Ko récupéré via le réseau pour les navigateurs plus récents

Seul le module est récupéré, avec une taille de bundle bien plus petite, car il n'est généralement pas transpilé. L'autre élément de script est complètement ignoré par le navigateur.

Si vous chargez l'application dans un navigateur plus ancien, seul le script plus grand et transpilé avec tous les polyfills et transformations nécessaires est récupéré. Voici une capture d'écran de toutes les requêtes effectuées sur une ancienne version de Chrome (version 38).

Script de 30 Ko récupéré pour les anciens navigateurs

Conclusion

Vous savez maintenant utiliser @babel/preset-env pour fournir uniquement les polyfills nécessaires aux navigateurs ciblés. Vous savez également comment les modules JavaScript peuvent améliorer davantage les performances en envoyant deux versions transpilées différentes d'une application. En comprenant bien comment ces deux techniques peuvent réduire considérablement la taille de votre lot, lancez-vous et optimisez-les.