Utiliser webpack pour réduire au maximum la taille de votre application
L'une des premières choses à faire lorsque vous optimisez une application est de la rendre aussi petite que possible. Voici comment procéder avec webpack.
Utiliser le mode de production (webpack 4 uniquement)
Webpack 4 a introduit le nouveau paramètre mode
. Vous pouvez définir cet indicateur sur 'development'
ou 'production'
pour indiquer à Webpack que vous compilez l'application pour un environnement spécifique:
// webpack.config.js
module.exports = {
mode: 'production',
};
Veillez à activer le mode production
lorsque vous compilez votre application pour la production.
Webpack appliquera alors des optimisations telles que la minimisation, la suppression du code réservé au développement dans les bibliothèques, et bien plus encore.
Documentation complémentaire
Activer la minification
La minimisation consiste à compresser le code en supprimant les espaces superflus, en raccourcissant les noms de variables, etc. Exemple :
// Original code
function map(array, iteratee) {
let index = -1;
const length = array == null ? 0 : array.length;
const result = new Array(length);
while (++index < length) {
result[index] = iteratee(array[index], index, array);
}
return result;
}
↓.
// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}
Webpack prend en charge deux méthodes pour réduire la taille du code: la minimisation au niveau du bundle et les options spécifiques au chargeur. Ils doivent être utilisés simultanément.
Minimisation au niveau du bundle
La minification au niveau du bundle compresse l'ensemble du bundle après compilation. Voici comment cela fonctionne :
Vous écrivez du code comme ceci:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack le compile approximativement comme suit:
// bundle.js (part of) "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony export (immutable) */ __webpack_exports__["render"] = render; /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__); function render(data, target) { console.log('Rendered!'); }
Un outil de réduction de taille le compresse approximativement dans ce qui suit:
// minified bundle.js (part of) "use strict";function t(e,n){console.log("Rendered!")} Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
Dans webpack 4, la minification au niveau du bundle est activée automatiquement, à la fois en mode production et sans mode de production. Il utilise le réducteur UglifyJS en arrière-plan. (Si vous devez désactiver la minimisation, utilisez simplement le mode Développement ou transmettez false
à l'option optimization.minimize
.)
Dans webpack 3, vous devez utiliser directement le plug-in UglifyJS. Le plug-in est fourni avec webpack. Pour l'activer, ajoutez-le à la section plugins
de la configuration :
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
Options spécifiques au chargeur
La deuxième méthode consiste à utiliser des options spécifiques au chargeur (qu'est-ce qu'un chargeur). Les options du chargeur vous permettent de compresser des éléments que le réducteur ne peut pas réduire. Par exemple, lorsque vous importez un fichier CSS avec css-loader
, le fichier est compilé dans une chaîne:
/* comments.css */
.comment {
color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
L'outil de réduction ne peut pas compresser ce code, car il s'agit d'une chaîne. Pour réduire la taille du contenu du fichier, nous devons configurer le chargeur de manière à ce qu'il effectue les opérations suivantes:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Documentation complémentaire
- Documentation UglifyJsPlugin
- Autres outils de réduction de taille courants: Babel Minify, Google Closure Compiler
Spécifier NODE_ENV=production
Une autre façon de réduire la taille de l'interface consiste à définir la variable d'environnement NODE_ENV
dans votre code sur la valeur production
.
Les bibliothèques lisent la variable NODE_ENV
pour détecter leur mode de fonctionnement (en développement ou de production). Certaines bibliothèques se comportent différemment en fonction de cette variable. Par exemple, lorsque NODE_ENV
n'est pas défini sur production
, Vue.js effectue des vérifications supplémentaires et affiche des avertissements:
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React fonctionne de la même manière. Elle charge un build de développement qui inclut les avertissements suivants:
// react/index.js
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
// react/cjs/react.development.js
// …
warning$3(
componentClass.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.'
);
// …
Ces vérifications et avertissements sont généralement inutiles en production, mais ils restent dans le code et augmentent la taille de la bibliothèque. Dans webpack 4,supprimez-les en ajoutant l'option optimization.nodeEnv: 'production'
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
Dans webpack 3,utilisez plutôt DefinePlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
L'option optimization.nodeEnv
et l'option DefinePlugin
fonctionnent de la même manière : elles remplacent toutes les occurrences de process.env.NODE_ENV
par la valeur spécifiée. Avec la configuration ci-dessus:
Webpack remplacera toutes les occurrences de
process.env.NODE_ENV
par"production"
:// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if (process.env.NODE_ENV !== 'production') { warn('props must be strings when using array syntax.'); }
↓.
// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
Le minificateur supprimera ensuite toutes ces branches
if
, car"production" !== 'production'
est toujours faux et que le plug-in comprend que le code de ces branches ne s'exécutera jamais :// vue/dist/vue.runtime.esm.js if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else if ("production" !== 'production') { warn('props must be strings when using array syntax.'); }
↓.
// vue/dist/vue.runtime.esm.js (without minification) if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; }
Documentation complémentaire
- Définition des "variables d'environnement"
- Documentation Webpack sur
DefinePlugin
etEnvironmentPlugin
Utiliser des modules ES
La deuxième façon de réduire la taille du front-end consiste à utiliser des modules ES.
Lorsque vous utilisez des modules ES, webpack peut effectuer un tree shaking. On parle de secouement d'arborescence lorsqu'un bundler parcourt l'intégralité de l'arborescence de dépendances, vérifie quelles dépendances sont utilisées et supprime celles qui ne sont pas utilisées. Ainsi, si vous utilisez la syntaxe du module ES, webpack peut éliminer le code inutilisé:
Vous écrivez un fichier avec plusieurs exportations, mais l'application n'en utilise qu'une seule :
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack comprend que
commentRestEndpoint
n'est pas utilisé et ne génère pas de point d'exportation distinct dans le bundle :// bundle.js (part that corresponds to comments.js) (function(module, __webpack_exports__, __webpack_require__) { "use strict"; const render = () => { return 'Rendered!'; }; /* harmony export (immutable) */ __webpack_exports__["a"] = render; const commentRestEndpoint = '/rest/comments'; /* unused harmony export commentRestEndpoint */ })
Le minificateur supprime la variable inutilisée :
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
Cela fonctionne même avec les bibliothèques si elles sont écrites avec des modules ES.
Vous n'êtes pas obligé d'utiliser précisément le minificateur intégré de webpack (UglifyJsPlugin
).
Tout outil de réduction de taille compatible avec la suppression des codes morts (par exemple, le plug-in Babel Minify ou le plug-in Google Closure Compiler) fera l'affaire.
Documentation complémentaire
Documentation Webpack sur le tree shaking
Optimiser les images
Les images représentent plus de la moitié de la taille de la page. Bien qu'ils ne soient pas aussi critiques que JavaScript (par exemple, ils ne bloquent pas le rendu), ils consomment toujours une grande partie de la bande passante. Utilisez url-loader
, svg-url-loader
et image-webpack-loader
pour les optimiser dans Webpack.
url-loader
intègre de petits fichiers statiques dans l'application. Sans configuration, il prend un fichier transmis, le place à côté du bundle compilé et renvoie une URL de ce fichier. Toutefois, si nous spécifions l'option limit
, elle encodera les fichiers de moins de 2 Mo en tant qu'URL de données Base64 et renverra cette URL. L'image est intégrée au code JavaScript et une requête HTTP est enregistrée :
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
},
},
],
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`
svg-url-loader
fonctionne exactement comme url-loader
, sauf qu'il encode les fichiers avec l'encodage URL au lieu de l'encodage base64. Cette approche est utile pour les images SVG, car les fichiers SVG ne sont qu'un texte brut, cet encodage est plus efficace en termes de taille.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
compresse les images qui y passent. Il est compatible avec les images JPG, PNG, GIF et SVG. Nous allons donc l'utiliser pour tous ces types.
Ce chargeur n'intègre pas d'images dans l'application. Il doit donc fonctionner en association avec url-loader
et svg-url-loader
. Pour éviter de copier et coller le code dans les deux règles (une pour les images JPG/PNG/GIF et une autre pour les images SVG), nous allons inclure ce chargeur en tant que règle distincte avec enforce: 'pre'
:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// This will apply the loader before the other ones
enforce: 'pre'
}
]
}
};
Les paramètres par défaut du chargeur sont déjà prêts, mais si vous souhaitez le configurer davantage, consultez les options du plug-in. Pour choisir les options à spécifier, consultez l'excellent guide d'optimisation des images d'Addy Osmani.
Documentation complémentaire
Optimiser les dépendances
Plus de la moitié de la taille moyenne du code JavaScript provient des dépendances, et une partie de cette taille peut être tout simplement inutile.
Par exemple, Lodash (à partir de la version 4.17.4) ajoute 72 ko de code minifié au bundle. Toutefois, si vous n'utilisez que 20 de ses méthodes, environ 65 ko de code minifié ne servent à rien.
Moment.js est un autre exemple. Sa version 2.19.1 occupe 223 ko de code réduit, ce qui est énorme. La taille moyenne du code JavaScript sur une page était de 452 ko en octobre 2017. Toutefois, 170 Ko correspond à des fichiers de localisation. Si vous n'utilisez pas Moment.js avec plusieurs langues, ces fichiers alourdiront le bundle sans raison.
Toutes ces dépendances peuvent être facilement optimisées. Nous avons rassemblé des approches d'optimisation dans un dépôt GitHub. Jetez-y un œil.
Activer la concaténation des modules ES (c'est-à-dire le hissage de champ d'application)
Lorsque vous créez un bundle, webpack encapsule chaque module dans une fonction :
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓.
// bundle.js (part of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_exports__["a"] = render;
function render(data, target) {
console.log('Rendered!');
}
})
Auparavant, cela était nécessaire pour isoler les modules CommonJS/AMD les uns des autres. Toutefois, cela a ajouté une surcharge de taille et de performances pour chaque module.
Webpack 2 a introduit la prise en charge des modules ES qui, contrairement aux modules CommonJS et AMD, peuvent être regroupés sans être encapsulés chacun avec une fonction. Webpack 3 a rendu ce regroupement possible, avec la concaténation des modules. Voici ce que fait la concaténation de modules :
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
↓.
// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files
// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// CONCATENATED MODULE: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// CONCATENATED MODULE: ./index.js
render();
})
Vous voyez la différence ? Dans le bundle simple, le module 0 nécessite l'autorisation render
du module 1. Avec la concaténation des modules, require
est simplement remplacé par la fonction requise et le module 1 est supprimé. Le bundle comporte moins de modules et moins de frais généraux.
Pour activer ce comportement, activez l'option optimization.concatenateModules
dans webpack 4:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
Dans webpack 3, utilisez ModuleConcatenationPlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Documentation complémentaire
- Documentation Webpack pour ModuleConcatenationPlugin
- "Brève introduction au hissage de champ d'application"
- Description détaillée du fonctionnement de ce plug-in
Utilisez externals
si vous avez à la fois du code webpack et un autre code
Vous pouvez avoir un projet volumineux dans lequel certains codes sont compilés avec webpack et d'autres non. Comme sur un site d'hébergement de vidéos, où le widget du lecteur peut être créé avec webpack, et où la page environnante peut ne pas être:
Si les deux éléments de code ont des dépendances communes, vous pouvez les partager pour éviter de télécharger leur code plusieurs fois. Pour ce faire, utilisez l'option externals
de webpack. Elle remplace les modules par des variables ou d'autres importations externes.
Si les dépendances sont disponibles dans window
Si votre code non webpack repose sur des dépendances disponibles en tant que variables dans window
, remplacez les noms de dépendance par des noms de variables :
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
Avec cette configuration, webpack ne regroupe pas les packages react
et react-dom
. À la place, ils seront remplacés par quelque chose comme ceci:
// bundle.js (part of)
(function(module, exports) {
// A module that exports `window.React`. Without `externals`,
// this module would include the whole React bundle
module.exports = React;
}),
(function(module, exports) {
// A module that exports `window.ReactDOM`. Without `externals`,
// this module would include the whole ReactDOM bundle
module.exports = ReactDOM;
})
Si les dépendances sont chargées en tant que packages AMD
Si le code autre que Webpack n'expose pas les dépendances dans window
, les choses sont plus compliquées.
Toutefois, vous pouvez toujours éviter de charger le même code deux fois si le code non-webpack utilise ces dépendances en tant que packages AMD.
Pour ce faire, compilez le code webpack en tant que bundle AMD et créez des alias de modules vers les URL de bibliothèque :
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack encapsule le bundle dans define()
et le fait dépendre des URL suivantes:
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
Si le code autre que Webpack utilise les mêmes URL pour charger ses dépendances, ces fichiers ne seront chargés qu'une seule fois. Les requêtes supplémentaires utiliseront le cache du chargeur.
Documentation complémentaire
- Documentation Webpack sur
externals
Récapitulatif
- Activer le mode production si vous utilisez webpack 4
- Minimisez votre code avec les options de réduction et de chargeur au niveau du bundle
- Supprimez le code réservé au développement en remplaçant
NODE_ENV
parproduction
. - Utiliser des modules ES pour activer le balayage de l'arborescence
- Compresser les images
- Appliquer des optimisations spécifiques aux dépendances
- Activer la concaténation des modules
- Utilisez
externals
si c'est judicieux pour vous.