A medida que su aplicación se desarrolla y crece, su tiempo de construcción también aumenta, desde varios minutos durante el reensamblaje en modo de desarrollo hasta decenas de minutos durante el ensamblaje de producción "en frío". Esto es completamente inaceptable. A los desarrolladores no nos gusta cambiar de contexto mientras esperamos que el paquete esté listo y queremos recibir comentarios de la aplicación lo antes posible, idealmente al cambiar del IDE al navegador.
¿Cómo lograr esto? ¿Qué podemos hacer para optimizar el tiempo de construcción?
Este artículo es una descripción general de las herramientas existentes en el ecosistema webpack para acelerar el ensamblaje, su experiencia y consejos.
La optimización del tamaño del paquete y el rendimiento de la aplicación en sí no se considera en este artículo.
El proyecto, cuyas referencias se encuentran en el texto y sobre qué mediciones de la velocidad de ensamblaje se realizan, es una aplicación relativamente pequeña escrita en la pila JS + Flow + React + Redux usando webpack, Babel, PostCSS, Sass, etc. y que consta de aproximadamente 30 mil líneas de código y 1,500 módulos. Las versiones de dependencia están vigentes a partir de abril de 2019.
Los estudios se realizaron en una computadora con Windows 10, Node.js 8, un procesador de 4 núcleos, 8 GB de memoria y SSD.
Terminología
- El ensamblaje es el proceso de convertir los archivos fuente del proyecto en un conjunto de activos relacionados que juntos forman una aplicación web.
- dev-mode: ensamblaje con el
mode: 'development'
opción mode: 'development'
, generalmente usando webpack-dev-server y watch-mode. - prod-mode: ensamblaje con el
mode: 'production'
opción mode: 'production'
, generalmente con un conjunto completo de optimizaciones de paquetes. - Compilación incremental - en modo dev: reconstruir solo archivos con cambios.
- Compilación "fría": compilación desde cero, sin cachés, pero con dependencias instaladas.
Almacenamiento en caché
El almacenamiento en caché le permite guardar los resultados de los cálculos para su posterior reutilización. El primer ensamblaje puede ser un poco más lento de lo habitual debido a la sobrecarga del almacenamiento en caché, pero los siguientes serán mucho más rápidos debido a la reutilización de los resultados de compilar módulos sin cambios.
De forma predeterminada, el paquete web en modo de observación almacena en caché los resultados de compilación intermedios en memoria para no volver a ensamblar todo el proyecto con cada cambio. Para una compilación normal (no en modo reloj), esta configuración no tiene sentido. También puede intentar activar la resolución de caché para simplificar la búsqueda de módulos en el paquete web y ver si esta configuración tiene un efecto notable en su proyecto.
No hay caché persistente (guardado en el disco u otro almacenamiento) en el paquete web, aunque prometen agregarlo en la versión 5. Mientras tanto, podemos usar las siguientes herramientas:
- Almacenamiento en caché en la configuración de TerserWebpackPlugin
Deshabilitado por defecto. Incluso solo tiene un efecto positivo notable: 60.7 s → 39 s (-36%), va bien con otras herramientas de almacenamiento en caché.
Encender y usar es muy simple:
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, cache: true }) ] }
- cargador de caché
El cargador de caché se puede colocar en cualquier cadena de cargadores y almacenar en caché los resultados de los cargadores anteriores.
Por defecto, guarda el caché en la carpeta .cache-loader en la raíz del proyecto. Usando la opción cacheDirectory
en la configuración del cargador, la ruta se puede redefinir.
Ejemplo de uso:
{ test: /\.js$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve( __dirname, 'node_modules/.cache/cache-loader' ), }, }, 'babel-loader' ] }
Solución segura y confiable. Funciona sin problemas con casi cualquier cargador: para scripts (babel-loader, ts-loader), estilos (scss-, less-, postcss-, css-loader), imágenes y fuentes (image-webpack-loader, react-svg- cargador, cargador de archivos), etc.
Por favor tenga en cuenta:
- Cuando se usa el cargador de caché junto con el cargador de estilo o MiniCssExtractPlugin.loader, debe colocarse después de ellos:
['style-loader', 'cache-loader', 'css-loader', ...]
. - Contrariamente a las recomendaciones de la documentación para usar este cargador para almacenar en caché los resultados de cálculos laboriosos, bien puede dar un aumento de rendimiento pequeño pero medible para los cargadores "más ligeros": debe probar y medir.
Resultados:
- dev: 35.5 s → (habilitar el cargador de caché) → 36.2 s (+ 2%) → (reensamblaje) → 7.9 s (-78%)
- prod: 60.6 s → (habilitar el cargador de caché) → 61.5 s (+ 1.5%) → (reensamblaje) → 30.6 s (-49%) → (activar caché para Terser) → 15, 4 s (-75%)
- HardSourceWebpackPlugin
Una solución más masiva e "inteligente" para el almacenamiento en caché a nivel de todo el proceso de ensamblaje, en lugar de cadenas individuales de cargadores. En el caso de uso básico, es suficiente agregar el complemento a la configuración del paquete web, la configuración estándar debería ser suficiente para la operación correcta. Adecuado para aquellos que desean alcanzar el máximo rendimiento y no temen enfrentar dificultades.
plugins: [ ..., new HardSourceWebpackPlugin() ]
La documentación contiene ejemplos de uso con configuraciones avanzadas y consejos para resolver posibles problemas. Antes de poner en funcionamiento el complemento de forma continua, vale la pena probar a fondo su funcionamiento en diversas situaciones y modos de montaje.
Resultados:
- dev: 35.5 s → (habilitar el complemento) → 36.5 s (+ 3%) → (reensamblaje) → 3.7 s (-90%)
- producto: 60.6 s → (encienda el complemento) → 69.5 s (+ 15%) → (reensamblaje) → 25 s (-59%) → (encienda el caché para Terser) → 10 s (-83%)
Pros:
- En comparación con el cargador de caché, acelera aún más el reensamblaje;
- No requiere declaraciones duplicadas en diferentes lugares de la configuración, como en el cargador de caché.
Contras:
- En comparación con el cargador de caché, ralentiza más la primera compilación (cuando no hay caché de disco);
- puede aumentar ligeramente el tiempo de reconstrucción incremental;
- puede causar problemas al usar webpack-dev-server y requerir una configuración detallada de la separación e invalidación de caché (ver documentación );
- bastantes problemas con errores en GitHub.
- Almacenamiento en caché en la configuración del cargador de babel . Deshabilitado por defecto. El efecto es varios por ciento peor que el del cargador de caché.
- Almacenamiento en caché en la configuración de eslint-loader . Deshabilitado por defecto. Si usa este cargador, el caché lo ayudará a no perder el tiempo alineando archivos sin cambios durante el reensamblaje.
Al usar el cargador de caché o HardSourceWebpackPlugin, debe deshabilitar los mecanismos de almacenamiento en caché integrados en otros complementos o cargadores (excepto TerserWebpackPlugin), ya que dejarán de ser útiles en compilaciones repetidas e incrementales, y los "fríos" incluso se ralentizarán. Lo mismo se aplica al cargador de caché en sí mismo si HardSourceWebpackPlugin ya está en uso.
Al configurar el almacenamiento en caché, pueden surgir las siguientes preguntas:
¿Dónde se deben almacenar los resultados del almacenamiento en caché?
node_modules/.cache/<_>/
generalmente se almacenan en el node_modules/.cache/<_>/
. La mayoría de las herramientas usan esta ruta de forma predeterminada y le permiten anularla si desea almacenar el caché en otro lugar.
¿Cuándo y cómo invalidar el caché?
Es muy importante vaciar el caché cuando se realizan cambios en la configuración del ensamblaje, lo que afectará la salida. El uso de la memoria caché anterior en estos casos es perjudicial y puede provocar errores de naturaleza desconocida.
Factores a considerar:
- lista de dependencias y sus versiones: package.json, package-lock.json, yarn.lock, .yarn-integridad;
- contenido de webpack, Babel, PostCSS, lista de navegadores y otros archivos de configuración que los cargadores y complementos utilizan explícita o implícitamente.
Si no utiliza el cargador de caché o HardSourceWebpackPlugin, que le permiten redefinir la lista de fuentes para formar la huella digital del ensamblado, los scripts npm que borran el caché al agregar, actualizar o eliminar dependencias lo ayudarán un poco más fácilmente:
"prunecaches": "rimraf ./node_modules/.cache/", "postinstall": "npm run prunecaches", "postuninstall": "npm run prunecaches"
Nodemon configurado para borrar el caché y reiniciar webpack-dev-server cuando detecta cambios en los archivos de configuración también ayudará:
"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" } }
¿Necesito guardar el caché en el repositorio del proyecto?
Como el caché es, de hecho, un artefacto de ensamblaje, no es necesario confirmarlo en el repositorio. La ubicación de la memoria caché dentro de la carpeta node_modules, que, como regla, se incluye en .gitignore, ayudará con esto.
Vale la pena señalar que si hubiera un sistema de almacenamiento en caché que pudiera determinar de manera confiable la validez de la memoria caché en cualquier condición, incluido el cambio del sistema operativo y la versión de Node.js, la memoria caché podría reutilizarse entre máquinas de desarrollo o en CI, lo que reduciría drásticamente el tiempo incluso desde la primera compilación después cambio entre ramas.
¿En qué modos de construcción vale la pena y en los que no vale la pena usar un caché?
No hay una respuesta definitiva aquí: todo depende de la intensidad con la que use los modos dev y prod durante el desarrollo y cambie entre ellos. En general, nada impide activar el almacenamiento en caché en todas partes, pero recuerde que generalmente ralentiza la primera compilación. En CI, probablemente siempre necesite una compilación "limpia", en cuyo caso el almacenamiento en caché se puede deshabilitar utilizando la variable de entorno adecuada.
Materiales interesantes sobre el almacenamiento en caché en webpack:
Paralelización
Usando la paralelización, puede obtener un aumento de rendimiento al usar todos los núcleos de procesador disponibles. El efecto final es individual para cada automóvil.
Por cierto, aquí hay un código simple Node.js para obtener la cantidad de núcleos de procesador disponibles (puede ser útil al configurar las herramientas que se enumeran a continuación):
const os = require('os'); const cores = os.cpus().length;
- Paralelización en la configuración de TerserWebpackPlugin
Deshabilitado por defecto. Además de su propio almacenamiento en caché, se enciende fácilmente y acelera notablemente el ensamblaje.
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, parallel: true }) ] }
- cargador de hilos
Thread-loader se puede colocar en una cadena de cargadores que realizan cálculos pesados, después de lo cual los cargadores anteriores usarán el grupo de subprocesos Node.js (procesadores).
Tiene un conjunto de opciones que le permiten ajustar el trabajo del grupo de trabajadores, aunque los valores básicos parecen bastante adecuados. poolTimeout
y los workers
merecen una atención especial; vea un ejemplo .
Se puede usar junto con el cargador de caché de la siguiente manera (el orden es importante): ['cache-loader', 'thread-loader', 'babel-loader']
. Si el "calentamiento" está habilitado para el cargador de subprocesos, debe verificar la estabilidad de los ensamblajes repetidos utilizando el caché; el paquete web puede bloquearse y no completar el proceso después de que el ensamblaje se haya completado con éxito. En este caso, simplemente apague el calentamiento.
Si encuentra un bloqueo de compilación después de agregar un cargador de hilos a la cadena de compilación de estilo Sass, este consejo podría ayudarlo.
- happypack
Un complemento que intercepta las llamadas de los cargadores y distribuye su trabajo en varios subprocesos. Por el momento, está en modo de soporte (es decir, el desarrollo no está planeado), y su creador recomienda el cargador de subprocesos como reemplazo. Por lo tanto, si su proyecto se mantiene actualizado, es mejor abstenerse de usar HappyPack, aunque vale la pena probar y comparar los resultados con el cargador de hilos.
HappyPack tiene documentación de configuración comprensible, que, por cierto, es bastante inusual en sí misma: se propone mover las configuraciones del cargador a la llamada del constructor del complemento y reemplazar las cadenas del cargador por su propio cargador happypack. Tal enfoque no estándar puede causar inconvenientes al crear una configuración personalizada "desde piezas" del paquete web.
HappyPack admite una lista limitada de cargadores ; Los principales y más utilizados en esta lista están presentes, pero el rendimiento de otros no está garantizado debido a la posible incompatibilidad de la API. Se puede encontrar más información en los temas del proyecto.
Denegación de cálculos
Cualquier trabajo lleva tiempo. Para pasar menos tiempo, debe evitar el trabajo que es de poca utilidad, puede posponerse hasta más tarde o no ser necesario en esta situación.
- Aplicar cargadores a la menor cantidad de módulos posible
Las propiedades de prueba, exclusión e inclusión especifican las condiciones para que el cargador incluya el módulo en el proceso de procesamiento. El punto es evitar la transformación de módulos que no necesitan esta transformación.
Un ejemplo popular es la excepción de node_modules de la transpilación a través de Babel:
rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' } ]
Otro ejemplo es que los archivos CSS comunes no necesitan ser procesados por un preprocesador:
rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ]
- No habilite optimizaciones de tamaño de paquete en modo dev
En una máquina de desarrollador potente con Internet estable, una aplicación implementada localmente generalmente se inicia rápidamente, incluso si pesa unos pocos megabytes. Optimizar un paquete durante el ensamblaje puede tomar mucho más tiempo valioso que ahorrar en carga.
El consejo se refiere a JS (Terser, Uglify , etc. ), CSS (cssnano, optimizar-css-assets-webpack-plugin), SVG e imágenes (SVGO, Imagemin, image-webpack-loader), HTML (html-minifier, opción en html-webpack-plugin), etc.
- No incluya polyfills y transformaciones en modo dev
Si usa babel-preset-env, postcss-preset-env o Autoprefixer, agregue una configuración de Browserslist separada para dev-mode, que incluye solo los navegadores que usa durante el desarrollo. Lo más probable es que estas sean las últimas versiones de Chrome o Firefox que soporten perfectamente los estándares modernos sin polyfills y transformaciones. Esto evitará trabajos innecesarios.
Ejemplo .browserslistrc:
[production] your supported browsers go here... [development] last 2 Chrome versions last 2 Firefox versions last 1 Safari version
- Revisar el uso de mapas fuente
Generar los mapas fuente más precisos y completos lleva un tiempo considerable (en nuestro proyecto, aproximadamente el 30% del tiempo de construcción de productos con la devtool: 'source-map'
). Piense si necesita mapas de origen en el ensamblaje de productos (localmente y en CI). Puede valer la pena generarlos solo cuando sea necesario, por ejemplo, en función de una variable de entorno o etiqueta en la confirmación.
En el modo de desarrollo, en la mayoría de los casos habrá una opción bastante ligera: 'cheap-eval-source-map'
o 'cheap-module-eval-source-map'
. Consulte la documentación del paquete web para obtener más detalles.
- Configurar la compresión en Terser
De acuerdo con la documentación de Terser (lo mismo se aplica a Uglify), cuando se minimiza el código, las opciones de compress
y compress
consumen una gran parte del tiempo. Al ajustarlos, puede lograr la aceleración del ensamblaje a costa de un ligero aumento en el tamaño del paquete. Hay un ejemplo en las fuentes de vue-cli y otro ejemplo de un ingeniero de Slack. En nuestro proyecto, el ajuste de Terser en la primera realización reduce el tiempo de montaje en aproximadamente un 7% a cambio de un aumento del 2,5 por ciento en el tamaño del paquete. Si el juego vale la pena, depende de usted.
- Excluir las dependencias externas del análisis
Usando las resolve.alias
module.noParse
y resolve.alias
puede redirigir la importación de módulos de biblioteca a versiones ya compiladas y simplemente insertarlas en el paquete sin perder tiempo analizando. En modo dev, esto debería aumentar significativamente la velocidad de ensamblaje, incluido el incremental.
El algoritmo es aproximadamente el siguiente:
(1) Haga una lista de módulos que deben omitirse al analizar.
Idealmente, estas son todas las dependencias de tiempo de ejecución que se incluyen en el paquete (o al menos las más masivas de ellas, como react-dom o lodash), y no solo las suyas (primer nivel), sino también las transitivas (dependencias de dependencia). En el futuro, deberá mantener esta lista usted mismo.
(2) Para los módulos seleccionados, escriba las rutas a sus versiones compiladas.
En lugar de omitir dependencias, debe proporcionar al recopilador una alternativa, y esta alternativa no debe depender del entorno: tener llamadas a module.exports
, require
, process
, import
, etc. Los módulos de un solo archivo precompilados (no necesariamente minimizados), que generalmente se encuentran en la carpeta dist dentro de las fuentes de dependencia, son adecuados para este rol. Para encontrarlos, debes ir a node_modules. Por ejemplo, para axios, la ruta al módulo compilado se ve así: node_modules/axios/dist/axios.js
.
(3) En la configuración del paquete web, use la opción resolve.alias para reemplazar las importaciones por nombres de dependencia con importaciones directas de archivos cuyas rutas se escribieron en el paso anterior.
Por ejemplo:
{ resolve: { alias: { axios: path.resolve( __dirname, 'node_modules/dist/axios.min.js' ), ... } } }
Aquí hay una gran falla: si su código o el código de sus dependencias no accede al punto de entrada estándar (archivo de índice, campo main
en package.json
), sino a un archivo específico dentro de las fuentes de dependencia, o si la dependencia se exporta como un módulo ES, o si el proceso de resolución está interfiriendo con algo (por ejemplo, babel-plugin-transform-imports), toda la idea puede fallar. El paquete se ensamblará, pero la aplicación se romperá.
(4) En la configuración del paquete web, use la opción module.noParse para omitir el análisis de módulos precompilados solicitados por las rutas del paso 2 utilizando expresiones regulares.
Por ejemplo:
{ module: { noParse: [ new RegExp('node_modules/dist/axios.min.js'), ... ] } }
En pocas palabras: en el papel, el método parece prometedor, pero una configuración no trivial con dificultades al menos aumenta los costos de implementación y, al menos, reduce los beneficios.
Una alternativa con un principio de funcionamiento similar es utilizar la opción externals
. En este caso, deberá insertar de forma independiente enlaces a secuencias de comandos externas en el archivo HTML, e incluso con las versiones de dependencia necesarias correspondientes a package.json.
- Separe el código que rara vez cambia en un paquete separado y compílelo solo una vez
Seguramente escuchaste sobre DllPlugin . Con él, puede distribuir código que cambia activamente (su aplicación) y rara vez cambia código (por ejemplo, dependencias) en diferentes ensamblajes. Una vez que el paquete de dependencias ensamblado (la misma DLL) simplemente se conecta al ensamblaje de la aplicación, se ahorra tiempo.
Se ve así en términos generales:
- Para construir la DLL, se crea una configuración de paquete web independiente, los módulos necesarios se conectan como puntos de entrada.
- La construcción comienza con esta configuración. DllPlugin genera un paquete DLL y un archivo de manifiesto con nombres de mapas y rutas de módulos.
- DllReferencePlugin se agrega a la configuración del ensamblado principal, al que se pasa el manifiesto.
- Las importaciones de dependencias representadas en archivos DLL durante el ensamblado se asignan a módulos ya compilados utilizando el manifiesto.
Puedes leer un poco más en el artículo aquí .
Al comenzar a utilizar este enfoque, encontrará rápidamente una serie de desventajas:
- El ensamblado de DLL está aislado del ensamblaje principal y debe administrarse por separado: prepare una configuración especial, reinícielo cada vez que se cambie una rama o cambie una dependencia.
- Dado que el archivo DLL no está relacionado con los artefactos del ensamblaje principal, deberá copiarse manualmente a la carpeta con los otros activos e incluirse en el archivo HTML utilizando uno de estos complementos: 1 , 2 .
- Es necesario mantener actualizada manualmente la lista de dependencias destinadas a ser incluidas en el paquete DLL.
- Lo más triste: el movimiento de los árboles no se aplica al paquete DLL. En teoría, la opción
entryOnly
está destinada a esto, pero olvidaron documentarla.
Puede deshacerse de la repetitiva y resolver el primer problema (así como el segundo, si usa html-webpack-plugin v3, no funciona con la versión 4) usando AutoDllPlugin . Sin embargo, todavía no es compatible con la opción entryOnly
para el entryOnly
utilizado "bajo el capó", y el autor del complemento duda de la conveniencia de usar su creación a la luz del próximo paquete web 5.
Misceláneo
Actualice su software y dependencias regularmente. 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 ejemplo:
{ 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 .) — !
Fuentes
, , , .