Dans cet atelier de programmation, améliorez les performances de cette application simple qui permet aux utilisateurs d'évaluer des chats aléatoires. Découvrez comment optimiser le bundle JavaScript en minimisant la quantité de code transcompilé.
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 actuelle du chat.
Mesurer
Il est toujours bon de commencer par inspecter un site Web avant d'ajouter des optimisations:
- Pour prévisualiser le site, appuyez sur View App (Afficher l'application), puis sur Fullscreen (Plein écran) .
- Appuyez sur Ctrl+Maj+J (ou Cmd+Option+J sur Mac) pour ouvrir les outils de développement.
- Cliquez sur l'onglet Réseau.
- Cochez la case Disable cache (Désactiver le cache).
- Actualisez l'application.
Plus de 80 Ko sont utilisés pour cette application. Voyons si certaines parties du bundle ne sont pas utilisées:
Appuyez sur
Control+Shift+P
(ouCommand+Shift+P
sur Mac) pour ouvrir le menu Commande.Saisissez
Show Coverage
, puis appuyez surEnter
pour afficher l'onglet Couverture.Dans l'onglet Couverture, cliquez sur Actualiser pour actualiser l'application tout en capturant la couverture.
Comparez la quantité de code utilisée et celle chargée pour le bundle principal:
Plus de la moitié du package (44 Ko) n'est même pas utilisé. En effet, une grande partie du code qu'il contient est constituée 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 à la norme ECMAScript (ou ECMA-262). Des versions plus récentes de la spécification sont publiées chaque année et incluent de nouvelles fonctionnalités qui ont passé 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 à explorer 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 les acceptent pas ? Babel, qui est incluse dans l'application, est la bibliothèque la plus populaire utilisée pour compiler du code contenant une syntaxe plus récente en un code que les anciens navigateurs et environnements peuvent comprendre. Pour ce faire, il procède de deux façons:
- Les polyfills sont inclus pour émuler les nouvelles fonctions ES2015+ 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 une version ultérieure) en une ancienne syntaxe ES5. Étant donné qu'il s'agit de modifications liées à la syntaxe (comme les fonctions fléchées), elles ne peuvent pas être émulées avec des polyfills.
Examinez package.json
pour voir 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. Ainsi, toutes les configurations Babel sont définies dans un.babelrc
à la racine du projet.babel-loader
inclut Babel dans le processus de compilation du webpack.
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 nouvelles fonctionnalités ECMAScript afin qu'elles puissent fonctionner dans des environnements qui ne sont pas compatibles. Il est déjà importé tout en haut desrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
identifie les transformations et les polyfills nécessaires pour les navigateurs ou environnements choisis comme cibles.
Examinez le fichier de configuration Babel, .babelrc
, pour en savoir plus:
{
"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 à browserlist. 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éboguer des modèles
Pour obtenir un aperçu complet de toutes les cibles Babel du navigateur, ainsi que de l'ensemble des transformations et des 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 Glitch en bas de l'éditeur.
Navigateurs ciblés
Babel enregistre à la console un certain nombre d'informations sur le processus de compilation, y compris tous les environnements cibles pour lesquels le code a été compilé.
Notez que les navigateurs obsolètes, tels qu'Internet Explorer, sont inclus dans cette liste. C'est un problème, car les navigateurs non compatibles ne disposeront pas de fonctionnalités plus récentes, et Babel continue à transpiler une syntaxe spécifique pour eux. Cela augmente inutilement la taille de votre bundle si les utilisateurs n'utilisent pas ce navigateur pour accéder à votre site.
Babel enregistre également la liste des plug-ins de transformation utilisés:
La liste est assez longue ! Voici tous les plug-ins dont Babel a besoin pour transformer une syntaxe ES2015+ en une syntaxe plus ancienne pour tous les navigateurs ciblés.
Toutefois, Babel n'affiche aucun polyfill spécifique utilisé:
En effet, l'intégralité de @babel/polyfill
est importée directement.
Charger des 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 aux navigateurs cibles, ajoutez un useBuiltIns: 'entry'
à la configuration.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
Rechargez l'application. Vous pouvez désormais voir tous les polyfills spécifiques inclus:
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é plus 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"
}
]
]
}
Les polyfills sont ainsi 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 requis pour l'application sont inclus.
La taille de l'app bundle est considérablement réduite.
Limitation de la liste des navigateurs compatibles
Le nombre de navigateurs cibles inclus reste assez élevé, et peu d'utilisateurs utilisent des navigateurs arrêté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é.
Comme l'application est si petite, il n'y a pas vraiment de différence avec ces modifications. Toutefois, nous vous recommandons d'utiliser un pourcentage de part de marché des navigateurs (tel que ">0.25%"
) et d'exclure des navigateurs spécifiques que vos utilisateurs n'utilisent pas, selon vous, est l'approche recommandée. Consultez l'article Les deux dernières versions" considérées comme dangereuses de James Kyle pour en savoir plus.
Utiliser <script type="module">
Il reste encore beaucoup à faire. Bien qu'un certain nombre de polyfills inutilisés aient été supprimés, beaucoup d'entre eux sont en cours de distribution et ne sont pas nécessaires à 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 peuvent être importés et exportés à 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 nouvelles fonctionnalités ECMAScript 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 Babel peut être modifiée pour envoyer deux versions différentes de votre application au navigateur:
- Version qui fonctionnerait dans les navigateurs plus récents qui prennent en charge les modules et qui comprend un module qui est en grande partie non transpilé mais dont la taille de fichier est plus petite
- Version incluant un script transpilé et plus volumineux qui fonctionne dans n'importe quel ancien navigateur
Utiliser des modules ES avec Babel
Pour définir 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 pack Web 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"
, esmodules
est utilisé avec la valeur false
. Cela signifie que Babel inclut toutes les transformations et tous les 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 transmis au 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 de configuration 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 ici 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", ce qui signifie que le code généré dans ce module est un script plus petit et moins compilé qui ne passe par aucune transformation dans cet exemple, car toutes les fonctionnalités utilisées sont déjà compatibles avec les navigateurs compatibles avec les modules.
À la toute fin du fichier, exportez les deux configurations dans un seul tableau.
module.exports = [
legacyConfig, moduleConfig
];
Cela permet de créer à la fois 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 dotés d'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 création de remplacement compilée. Idéalement, les deux versions de l'application doivent se trouver dans index.html
comme suit:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
Les navigateurs compatibles avec les modules extraient et exécutent main.mjs
, et ignorent main.bundle.js.
. Les navigateurs non compatibles avec les modules font l'inverse.
Notez que, contrairement aux scripts classiques, les scripts de module sont toujours différés par défaut.
Si vous souhaitez que le script nomodule
équivalent soit également différé et exécuté 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 chose à faire ici consiste à ajouter les attributs module
et nomodule
respectivement au module et au script hérité, en important ScriptExtHtmlWebpackPlugin tout en haut de 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");
Mettez maintenant à 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
.
Diffuser des modules dans le document HTML
La dernière étape consiste à afficher les anciens et les nouveaux éléments de script 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 du module et des scripts 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 consistant à ajouter manuellement l'élément de script du module est utilisée dans le cadre de 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, comme la dernière version de Chrome.
Seul le module est récupéré. La taille du bundle est bien inférieure, car il n'est pas transpilé en grande partie. 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 transpilé et de grande taille contenant 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).
Conclusion
Vous savez maintenant utiliser @babel/preset-env
pour ne fournir que 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. Si vous comprenez bien comment ces deux techniques peuvent réduire considérablement la taille du bundle, vous pouvez commencer à l'optimiser.