Au fur et à mesure que votre application se développe et se développe, son temps de construction augmente également - de plusieurs minutes lors du remontage en mode développement à des dizaines de minutes lors du montage en production «à froid». C'est totalement inacceptable. Nous, les développeurs, n'aimons pas changer de contexte en attendant que le bundle soit prêt et souhaitons recevoir les commentaires de l'application le plus tôt possible - idéalement lors du passage de l'IDE au navigateur.
Comment y parvenir? Que pouvons-nous faire pour optimiser le temps de construction?
Cet article est un aperçu des outils existants dans l'écosystème webpack pour accélérer l'assemblage, leur expérience et leurs conseils.
L'optimisation de la taille du bundle et des performances de l'application elle-mĂŞme n'est pas prise en compte dans cet article.
Le projet, dont les références se trouvent dans le texte et concernant les mesures de la vitesse d'assemblage qui sont effectuées, est une application relativement petite écrite sur la pile JS + Flow + React + Redux à l'aide de webpack, Babel, PostCSS, Sass, etc. et composée d'environ 30 000 lignes de code et 1 500 modules. Les versions de dépendance sont à jour en avril 2019.
Les études ont été menées sur un ordinateur avec Windows 10, Node.js 8, un processeur à 4 cœurs, 8 Go de mémoire et SSD.
Terminologie
- L'assemblage est le processus de conversion des fichiers source du projet en un ensemble d'actifs associés qui constituent ensemble une application Web.
- dev-mode - assemblage avec le
mode: 'development'
option mode: 'development'
, utilisant généralement webpack-dev-server et watch-mode. - prod-mode - assemblage avec le
mode: 'production'
option mode: 'production'
, généralement avec un ensemble complet d'optimisations de bundle. - Build incrémental - en mode dev: reconstruisez uniquement les fichiers avec des modifications.
- Génération «à froid» - génération à partir de zéro, sans cache, mais avec les dépendances installées.
Mise en cache
La mise en cache vous permet d'enregistrer les résultats des calculs pour une réutilisation ultérieure. Le premier assemblage peut être légèrement plus lent que d'habitude en raison de la surcharge de la mise en cache, mais les suivants seront beaucoup plus rapides en raison de la réutilisation des résultats de la compilation des modules inchangés.
Par défaut, webpack en mode veille met en cache les résultats de la construction intermédiaire en mémoire afin de ne pas réassembler le projet entier à chaque modification. Pour une version normale (pas en mode montre), ce paramètre n'a pas de sens. Vous pouvez également essayer d'activer la résolution du cache pour faciliter la recherche de modules par Webpack et voir si ce paramètre a un effet notable sur votre projet.
Il n'y a pas de cache persistant (enregistré sur disque ou autre stockage) dans webpack, bien qu'ils promettent de l'ajouter dans la version 5. En attendant, nous pouvons utiliser les outils suivants:
- Mise en cache dans les paramètres TerserWebpackPlugin
Désactivé par défaut. Même seul, il a un effet positif notable: 60,7 s → 39 s (-36%), va bien avec d'autres outils de mise en cache.
La mise sous tension et l'utilisation sont très simples:
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, cache: true }) ] }
- cache-loader
Le chargeur de cache peut être placé dans n'importe quelle chaîne de chargeurs et mettre en cache les résultats des chargeurs précédents.
Par défaut, il enregistre le cache dans le dossier .cache-loader à la racine du projet. En utilisant l'option cacheDirectory
dans les paramètres du chargeur, le chemin peut être redéfini.
Exemple d'utilisation:
{ test: /\.js$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve( __dirname, 'node_modules/.cache/cache-loader' ), }, }, 'babel-loader' ] }
Solution sûre et fiable. Il fonctionne sans problème avec presque tous les chargeurs: pour les scripts (babel-loader, ts-loader), les styles (scss-, less-, postcss-, css-loader), les images et les polices (image-webpack-loader, react-svg- chargeur, chargeur de fichiers), etc.
Veuillez noter:
- Lorsque vous utilisez cache-loader en conjonction avec style-loader ou MiniCssExtractPlugin.loader, il doit être placé après eux:
['style-loader', 'cache-loader', 'css-loader', ...]
. - Contrairement aux recommandations de la documentation d'utiliser ce chargeur pour mettre en cache uniquement les résultats de calculs laborieux, il peut très bien donner une augmentation de performances faible mais mesurable pour les chargeurs «plus légers» - vous devez essayer de mesurer.
Résultats:
- dev: 35,5 s → (activer le chargeur de cache) → 36,2 s (+ 2%) → (réassemblage) → 7,9 s (-78%)
- prod: 60,6 s → (activer le chargeur de cache) → 61,5 s (+ 1,5%) → (réassemblage) → 30,6 s (-49%) → (activer le cache pour Terser) → 15, 4 s (-75%)
- HardSourceWebpackPlugin
Une solution plus massive et «intelligente» pour la mise en cache au niveau de l'ensemble du processus d'assemblage, plutôt que des chaînes individuelles de chargeurs. Dans le cas d'utilisation de base, il suffit d'ajouter le plugin à la configuration du webpack, les paramètres standard devraient être suffisants pour le bon fonctionnement. Convient à ceux qui souhaitent atteindre des performances maximales et n'ont pas peur de faire face à des difficultés.
plugins: [ ..., new HardSourceWebpackPlugin() ]
La documentation contient des exemples d'utilisation avec des paramètres avancés et des conseils pour résoudre d'éventuels problèmes. Avant de mettre le plug-in en service de manière continue, il convient de tester minutieusement son fonctionnement dans diverses situations et modes d'assemblage.
Résultats:
- dev: 35,5 s → (activer le plugin) → 36,5 s (+ 3%) → (remontage) → 3,7 s (-90%)
- prod: 60,6 s → (activer le plugin) → 69,5 s (+ 15%) → (remonter) → 25 s (-59%) → (activer le cache pour Terser) → 10 s (-83%)
Avantages:
- Comparé au cache-chargeur, il accélère encore plus le réassemblage;
- Il ne nécessite pas de déclarations en double à différents endroits de la configuration, comme dans le chargeur de cache
Inconvénients:
- Comparé au chargeur de cache, il ralentit davantage la première génération (lorsqu'il n'y a pas de cache disque);
- peut augmenter légèrement le temps de reconstruction incrémentiel;
- peut provoquer des problèmes lors de l'utilisation de webpack-dev-server et nécessiter une configuration détaillée de la séparation du cache et de l'invalidation (voir la documentation );
- pas mal de problèmes avec les bugs sur GitHub.
- Mise en cache dans les paramètres du chargeur de babel . Désactivé par défaut. L'effet est de plusieurs pour cent pire que celui du chargeur de cache.
- Mise en cache dans les paramètres du chargeur eslint . Désactivé par défaut. Si vous utilisez ce chargeur, le cache vous aidera à ne pas perdre de temps à aligner des fichiers inchangés lors du réassemblage.
Lorsque vous utilisez le chargeur de cache ou HardSourceWebpackPlugin, vous devez désactiver les mécanismes de mise en cache intégrés dans d'autres plugins ou chargeurs (à l'exception de TerserWebpackPlugin), car ils cesseront d'être utiles dans les versions répétées et incrémentielles, et les versions «froides» ralentiront même. La même chose s'applique au cache-chargeur lui-même si HardSourceWebpackPlugin est déjà utilisé.
Lors de la configuration de la mise en cache, les questions suivantes peuvent se poser:
Où les résultats de mise en cache doivent-ils être stockés?
node_modules/.cache/<_>/
sont généralement stockés dans le node_modules/.cache/<_>/
. La plupart des outils utilisent ce chemin par défaut et vous permettent de le remplacer si vous souhaitez stocker le cache ailleurs.
Quand et comment invalider le cache?
Il est très important de vider le cache lorsque des modifications sont apportées à la configuration de génération, ce qui affectera la sortie. L'utilisation de l'ancien cache dans de tels cas est nuisible et peut entraîner des erreurs de nature inconnue.
Facteurs à considérer:
- liste des dépendances et de leurs versions: package.json, package-lock.json, yarn.lock, .yarn-Integrity;
- le contenu de webpack, Babel, PostCSS, Browserslist et d'autres fichiers de configuration qui sont explicitement ou implicitement utilisés par les chargeurs et les plugins.
Si vous n'utilisez pas le chargeur de cache ou HardSourceWebpackPlugin, qui vous permettent de redéfinir la liste des sources pour former l'empreinte digitale de l'assembly, les scripts npm qui effacent le cache lors de l'ajout, de la mise à jour ou de la suppression des dépendances vous aideront un peu plus facilement:
"prunecaches": "rimraf ./node_modules/.cache/", "postinstall": "npm run prunecaches", "postuninstall": "npm run prunecaches"
Nodemon configuré pour effacer le cache et redémarrer webpack-dev-server lors de la détection des changements dans les fichiers de configuration aidera également:
"start": "cross-env NODE_ENV=development nodemon --exec \"webpack-dev-server --config webpack.config.dev.js\""
nodemon.json
{ "watch": [ "webpack.config.dev.js", "babel.config.js", "more configs...", ], "events": { "restart": "yarn prunecaches" } }
Dois-je enregistrer le cache dans le référentiel du projet?
Étant donné que le cache est, en fait, un artefact d'assembly, il n'est pas nécessaire de le valider dans le référentiel. L'emplacement du cache dans le dossier node_modules, qui, en règle générale, est inclus dans .gitignore, vous y aidera.
Il convient de noter que s'il existait un système de mise en cache qui pourrait déterminer de manière fiable la validité du cache dans toutes les conditions, y compris le changement du système d'exploitation et de la version de Node.js, le cache pourrait être réutilisé entre des machines de développement ou dans CI, ce qui réduirait considérablement le temps même de la toute première construction après commutation entre les branches.
Dans quels modes de construction cela vaut-il et dans lequel cela ne vaut-il pas la peine d'utiliser un cache?
Il n'y a pas de réponse définitive ici: tout dépend de l'intensité avec laquelle vous utilisez les modes dev et prod pendant le développement et basculez entre eux. En général, rien n'empêche d'activer la mise en cache partout, mais n'oubliez pas que cela ralentit généralement la première génération. Dans CI, vous avez probablement toujours besoin d'une version «propre», auquel cas la mise en cache peut être désactivée à l'aide de la variable d'environnement appropriée.
Matériaux intéressants sur la mise en cache dans le webpack:
Parallélisation
En utilisant la parallélisation, vous pouvez obtenir une amélioration des performances en utilisant tous les cœurs de processeur disponibles. L'effet final est individuel pour chaque voiture.
À propos, voici un code Node.js simple pour obtenir le nombre de cœurs de processeur disponibles (il peut être utile lors de la configuration des outils répertoriés ci-dessous):
const os = require('os'); const cores = os.cpus().length;
- Parallélisation dans les paramètres TerserWebpackPlugin
Désactivé par défaut. En plus de sa propre mise en cache, il s'allume facilement et accélère sensiblement l'assemblage.
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, parallel: true }) ] }
- chargeur de fil
Le chargeur de threads peut être placé dans une chaîne de chargeurs qui effectuent des calculs lourds, après quoi les chargeurs précédents utiliseront le pool de sous-processus Node.js (processeurs).
Il dispose d'un ensemble d'options qui vous permettent d'affiner le travail du pool de travailleurs, bien que les valeurs de base semblent tout à fait adéquates. poolTimeout
et les workers
méritent une attention particulière - voir un exemple .
Il peut être utilisé avec cache-loader comme suit (l'ordre est important): ['cache-loader', 'thread-loader', 'babel-loader']
. Si le préchauffage est activé pour le chargeur de threads, vous devez vérifier la stabilité des assemblys répétés qui utilisent le cache - le webpack peut se bloquer et ne pas terminer le processus une fois l'assembly terminé avec succès. Dans ce cas, désactivez simplement l'échauffement.
Si vous rencontrez un blocage de build après avoir ajouté un chargeur de threads à la chaîne de compilation de style Sass, cette astuce pourrait vous aider.
- happypack
Un plugin qui intercepte les appels des chargeurs et distribue leur travail sur plusieurs threads. Pour le moment, il est en mode support (c'est-à -dire que le développement n'est pas prévu), et son créateur recommande le thread-loader en remplacement. Ainsi, si votre projet est à jour, il vaut mieux s'abstenir d'utiliser HappyPack, même s'il vaut certainement la peine d'essayer et de comparer les résultats avec le chargeur de threads.
HappyPack possède une documentation de configuration compréhensible, ce qui est d'ailleurs assez inhabituel en soi: il est proposé de déplacer les configurations du chargeur vers l'appel du constructeur du plug-in et de remplacer les chaînes de chargeur elles-mêmes par leur propre chargeur happypack. Une telle approche non standard peut entraîner des désagréments lors de la création d'une configuration Webpack personnalisée «à partir de pièces».
HappyPack prend en charge une liste limitée de chargeurs ; les principaux et les plus utilisés dans cette liste sont présents, mais les performances des autres ne sont pas garanties en raison d'une éventuelle incompatibilité de l'API. Plus d'informations peuvent être trouvées dans les numéros du projet.
Refus de calculs
Tout travail prend du temps. Pour passer moins de temps, vous devez éviter les travaux de peu d'utilité, qui peuvent être reportés à plus tard, ou pas du tout nécessaires dans cette situation.
- Appliquer des chargeurs sur le moins de modules possible
Les propriétés test, exclude et include spécifient les conditions d'inclusion du module dans le processus de traitement par le chargeur. Le but est d'éviter de transformer des modules qui n'ont pas besoin de cette transformation.
Un exemple populaire est l'exception de node_modules de la transpilation via Babel:
rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' } ]
Un autre exemple est que les fichiers CSS ordinaires n'ont pas besoin d'être traités par un préprocesseur:
rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ]
- Ne pas activer les optimisations de taille de bundle en mode dev
Sur une machine de développeur puissante avec un Internet stable, une application déployée localement démarre généralement rapidement, même si elle pèse quelques mégaoctets. L'optimisation d'un faisceau lors de l'assemblage peut prendre beaucoup plus de temps que d'économiser sur la charge.
Les conseils concernent JS (Terser, Uglify , etc. ), CSS (cssnano, optimiser-css-assets-webpack-plugin), SVG et images (SVGO, Imagemin, image-webpack-loader), HTML (html-minifier, option dans html-webpack-plugin), etc.
- Ne pas inclure les polyfills et les transformations en mode dev
Si vous utilisez babel-preset-env, postcss-preset-env ou Autoprefixer - ajoutez une configuration Browserslist distincte pour le mode dev, y compris uniquement les navigateurs que vous utilisez pendant le développement. Il s'agit très probablement des dernières versions de Chrome ou Firefox qui prennent parfaitement en charge les normes modernes sans polyfills et transformations. Cela évitera un travail inutile.
Exemple .browserslistrc:
[production] your supported browsers go here... [development] last 2 Chrome versions last 2 Firefox versions last 1 Safari version
- Revoir l'utilisation des cartes sources
La génération des cartes sources les plus précises et complètes prend beaucoup de temps (sur notre projet - environ 30% du temps de génération de prod avec l' devtool: 'source-map'
). Réfléchissez si vous avez besoin de cartes sources dans l'assemblage prod (localement et dans CI). Il peut être utile de les générer uniquement lorsque cela est nécessaire - par exemple, en fonction d'une variable d'environnement ou d'une balise lors de la validation.
En mode dev, dans la plupart des cas, il y aura une option plutôt légère - 'cheap-eval-source-map'
ou 'cheap-module-eval-source-map'
. Voir la documentation du webpack pour plus de détails.
- Configurer la compression dans Terser
Selon la documentation Terser (il en va de même pour Uglify), lors de la réduction du code, la majeure partie du temps est consommée par les options mangle
et compress
. En les affinant, vous pouvez obtenir une accélération de l'assemblage au prix d'une légère augmentation de la taille du faisceau. Il y a un exemple dans les sources de vue-cli et un autre exemple d'un ingénieur de Slack. Dans notre projet, le réglage Terser dans le premier mode de réalisation réduit le temps d'assemblage d'environ 7% en échange d'une augmentation de 2,5% de la taille du faisceau. Que le jeu en vaille la chandelle dépend de vous.
- Exclure les dépendances externes de l'analyse
En utilisant les resolve.alias
module.noParse
et module.noParse
resolve.alias
vous pouvez rediriger l'importation des modules de bibliothèque vers des versions déjà compilées et simplement les insérer dans le bundle sans perdre de temps à analyser. En mode dev, cela devrait augmenter considérablement la vitesse d'assemblage, y compris incrémentale.
L'algorithme est approximativement le suivant:
(1) Faites une liste des modules qui doivent être ignorés lors de l'analyse.
Idéalement, ce sont toutes des dépendances d'exécution qui tombent dans le bundle (ou du moins les plus massives d'entre elles, telles que react-dom ou lodash), et non seulement les leurs (premier niveau), mais aussi transitives (dépendances de dépendance). À l'avenir, vous devrez maintenir cette liste vous-même.
(2) Pour les modules sélectionnés, écrivez les chemins d'accès à leurs versions compilées.
Au lieu d'ignorer les dépendances, vous devez fournir au collecteur une alternative, et cette alternative ne devrait pas dépendre de l'environnement - avoir des appels à module.exports
, require
, process
, import
, etc. Les modules de fichier unique précompilés (pas nécessairement minifiés), qui se trouvent généralement dans le dossier dist à l'intérieur des sources de dépendance, conviennent à ce rôle. Pour les trouver, vous devez vous rendre sur node_modules. Par exemple, pour axios, le chemin d'accès au module compilé ressemble à ceci: node_modules/axios/dist/axios.js
.
(3) Dans la configuration du webpack, utilisez l'option resol.alias pour remplacer les importations par des noms de dépendances par des importations directes de fichiers dont les chemins ont été écrits à l'étape précédente.
Par exemple:
{ resolve: { alias: { axios: path.resolve( __dirname, 'node_modules/dist/axios.min.js' ), ... } } }
Il y a un gros défaut ici: si votre code ou le code de vos dépendances n'accède pas au point d'entrée standard (fichier d'index, champ main
dans package.json
), mais un fichier spécifique à l'intérieur des sources de dépendances, ou si la dépendance est exportée en tant que module ES, ou si le processus de résolution interfère avec quelque chose (par exemple, babel-plugin-transform-imports), l'idée peut échouer. Le bundle se réunira, mais l'application sera cassée.
(4) Dans la configuration du webpack, utilisez l'option module.noParse pour ignorer l'analyse des modules précompilés demandés par les chemins de l'étape 2 à l'aide d'expressions régulières.
Par exemple:
{ module: { noParse: [ new RegExp('node_modules/dist/axios.min.js'), ... ] } }
Conclusion: sur le papier, la méthode semble prometteuse, mais une configuration non triviale avec des pièges augmente au moins les coûts de mise en œuvre, et à tout le moins réduit les avantages.
Une alternative avec un principe de fonctionnement similaire consiste Ă utiliser l'option externals
. Dans ce cas, vous devrez insérer indépendamment des liens vers des scripts externes dans le fichier HTML, et même avec les versions de dépendance nécessaires correspondant à package.json.
- Séparez rarement le code changeant dans un bundle séparé et compilez-le une seule fois
Vous avez sûrement entendu parler de DllPlugin . Avec lui, vous pouvez distribuer du code changeant activement (votre application) et rarement du code (par exemple, des dépendances) dans différents assemblys. Une fois que le bundle de dépendances assemblé (la même DLL) est ensuite simplement connecté à l'assembly d'application, il fait gagner du temps.
Cela ressemble à ceci en termes généraux:
- Pour créer la DLL, une configuration Webpack distincte est créée, les modules nécessaires sont connectés en tant que points d'entrée.
- La construction commence avec cette configuration. DllPlugin génère un bundle DLL et un fichier manifeste avec des noms de carte et des chemins de module.
- DllReferencePlugin est ajouté à la configuration de l'assembly principal, dans lequel le manifeste est passé.
- Les importations de dépendances rendues dans les DLL pendant l'assemblage sont mappées sur des modules déjà compilés à l'aide du manifeste.
Vous pouvez en lire un peu plus dans l'article ici .
En commençant à utiliser cette approche, vous trouverez rapidement un certain nombre d'inconvénients:
- L'assembly DLL est isolé de l'assembly principal et doit être géré séparément: préparez une configuration spéciale, redémarrez-la chaque fois qu'une branche est commutée ou qu'une dépendance change.
- Étant donné que la DLL n'est pas liée aux artefacts de l'assembly principal, elle devra être copiée manuellement dans le dossier avec les autres actifs et incluse dans le fichier HTML à l'aide de l'un de ces plugins: 1 , 2 .
- Il est nécessaire de maintenir manuellement à jour la liste des dépendances destinées à être incluses dans le bundle DLL.
- La chose la plus triste: l'arborescence n'est pas appliquée au bundle DLL. En théorie, l'option
entryOnly
est destinée à cela, mais ils ont oublié de la documenter.
Vous pouvez vous débarrasser du passe-partout et résoudre le premier problème (ainsi que le second, si vous utilisez html-webpack-plugin v3 - cela ne fonctionne pas avec la version 4) en utilisant AutoDllPlugin . Cependant, il ne prend toujours pas en charge l'option entryOnly
pour le entryOnly
utilisé "sous le capot", et l'auteur du plugin doute de l'opportunité d'utiliser son idée à la lumière du prochain webpack 5.
Divers
Mettez régulièrement à jour votre logiciel et vos dépendances. Node.js, npm / yarn (webpack, Babel .) . , changelog, issues, , .
PostCSS postcss-preset-env stage, . , stage-3, Custom Properties, stage-4 13%.
Sass (node-sass, sass-loader), Dart Sass ( Sass Dart, JS) fast-sass-loader . , . — dart-sass , node-sass, JS, libsass.
Dart Sass sass-loader . Sass fibers.
CSS-, dev-. - , , , .
Un exemple:
{ loader: 'css-loader', options: { modules: true, localIdentName: isDev ? '[path][name][local]' : '[hash:base64:5]' } }
, , : .
, - webpack PrefetchPlugin , , — . webpack issues , . ?
- . CLI-
--json
, . . , , dev- . - - Hints.
- , “Long module build chains”. , — PrefetchPlugin .
- PrefetchPlugin. . StackOverflow .
: .
, (TypeScript, Angular .) — !
Les sources
, , , .