Recolectando el paquete de sueños con Webpack

Las aplicaciones JS, los sitios y otros recursos se están volviendo más complejos y las herramientas de construcción son la realidad del desarrollo web. Los paquetes ayudan a empacar, compilar y organizar bibliotecas. Webpack es una de las herramientas de código abierto potentes y flexibles que se pueden personalizar perfectamente para crear la aplicación cliente.

Maxim Sosnov ( crazymax11 ) - Frontend Lead en N1.RU introdujo Webpack en varios proyectos grandes que anteriormente tenían su propia construcción personalizada, y contribuyó con varios proyectos. Maxim sabe cómo construir un paquete soñado con Webpack, hacerlo rápidamente y configurarlo para que la configuración permanezca limpia, mantenida y modular.


La interpretación es diferente del informe: es una versión muy mejorada de los enlaces de perfil. A lo largo de la transcripción, los huevos de Pascua están dispersos en artículos, complementos, minificadores, opciones, transpiladores y pruebas de las palabras del hablante, enlaces a los que simplemente no se pueden poner en un discurso. Si recoge todo, se abre el nivel de bonificación en Webpack :-)

Integración de webpack en un proyecto típico


Por lo general, el procedimiento de implementación es el siguiente: el desarrollador en algún lugar lee un artículo sobre Webpack, decide conectarlo, comienza a compilarlo, de alguna manera funciona, todo comienza y durante algún tiempo funciona webpack-config: durante seis meses, un año, dos. A nivel local, todo está bien: el sol, el arco iris y las mariposas. Y luego vienen los usuarios reales:

- Desde dispositivos móviles su sitio no se carga.
- Todo funciona para nosotros. A nivel local, todo está bien!

Por si acaso, el desarrollador analiza todo y ve que para dispositivos móviles el paquete pesa 7 MB y tarda 30 segundos en cargarse . Esto no se adapta a nadie y el desarrollador comienza a buscar cómo resolver el problema: puede conectar el cargador o encontrar un complemento mágico que resuelva todos los problemas. Milagrosamente, se encuentra dicho complemento. Nuestro desarrollador va a webpack-config, intenta instalar, pero la línea de código interfiere:

if (process.env.NODE_ENV === 'production') { config.module.rules[7].options.magic = true; } 

La línea se traduce de la siguiente manera: "Si la configuración se está ensamblando para producción, entonces tome la séptima regla y coloque la opción magic = true ". El desarrollador no sabe qué hacer con esto y cómo resolverlo. Esta es una situación en la que necesitas un paquete de sueños.

¿Cómo recoger un paquete de sueños?


Primero, definamos qué es. En primer lugar, el paquete de sueños tiene dos características principales:

  • Pesa un poco Cuanto menos peso, más rápido obtendrá el usuario una aplicación que funcione. No desea que su sitio se abra durante 15 segundos.
  • El usuario descarga solo lo que necesita descargar para mostrar la página actual del sitio, ¡y no un byte más!

Y para reducir el tamaño del paquete, primero debe evaluar su tamaño.

Calificar el tamaño del paquete


La solución más popular es el complemento WebpackBundleAnalyzer . Recopila estadísticas de compilación de aplicaciones y presenta una página interactiva donde puede ver la ubicación y el peso de cada módulo.

imagen

Si esto no es suficiente, puede crear un gráfico de dependencia utilizando otro complemento .

imagen

O un gráfico circular .

imagen

Si esto no es suficiente, y desea vender Webpack a los vendedores, entonces puede construir un universo entero donde cada punto es un módulo, como una estrella en el Universo.

imagen

Existen muchas herramientas que evalúan el tamaño del paquete y lo supervisan. Hay una opción en la configuración de Webpack que bloquea el ensamblaje si el paquete pesa demasiado, por ejemplo. Existe un complemento duplicado-paquete-corrector-paquete-web-plugin que le impedirá construir un paquete si tiene paquetes de 2 npm de diferentes versiones, por ejemplo, Lodash 4.15 y Lodash 4.14.

Cómo reducir el paquete


  • Lo más obvio es conectar UglifyJS para que minimice JavaScript.
  • Utilice cargadores y complementos especiales que comprimen y optimizan un recurso específico. Por ejemplo, css-nano para css o SVGO , que optimiza SVG.
  • Comprima todos los archivos directamente en Webpack a través de los complementos gzip / brotli .
  • Otras herramientas

Ahora entenderemos cómo arrojar el exceso del paquete.

Tirar el exceso


Considere esto en un ejemplo popular con moment.js : import moment from 'moment' . Si toma una aplicación vacía, importe moment.js y ReactDOM en ella, y luego la pase a través de WebpackBundleAnalyzer , verá la siguiente imagen.

imagen

Resulta que cuando agrega un día, una hora a una fecha, o simplemente desea poner el enlace "en 15 minutos" usando moment.js, ¡conecta un código de 230 Kbytes completo! ¿Por qué sucede esto y cómo se resuelve?

Carga regional en este momento


Hay una función en moment.js que establece las configuraciones regionales:

 function setLocale(locale) { const localePath = 'locale/' + locale + '.js'; this._currentLocale = require(localePath); } 

Se puede ver en el código que la configuración regional se carga a lo largo de la ruta dinámica, es decir, calculado en tiempo de ejecución. Webpack actúa de manera inteligente e intenta asegurarse de que su paquete no se bloquee durante la ejecución del código: encuentra todas las configuraciones regionales posibles en el proyecto y las agrupa. Por lo tanto, la aplicación pesa mucho.

imagen

La solución es muy simple: tomamos un complemento estándar de Webpack y le decimos: "Si ves que alguien quiere descargar muchas configuraciones regionales, porque no pueden determinar cuál, ¡toma la rusa!"

imagen

Webpack solo tomará ruso, y WebpackBundleAnalyzer mostrará 54 Kb, que ya es 200 Kb más fácil.

Eliminación de código muerto


La siguiente optimización que nos interesa es la eliminación del código muerto . Considere el siguiente código.

 const cond = true; if (!cond) { return false; } return true; someFunction(42); 

La mayoría de las líneas de este código no son necesarias en el paquete final: el bloque con la condición no se ejecutará, la función después del retorno también. Todo lo que necesitas para salir es return true . Esto es exactamente lo que es la eliminación del código muerto: la herramienta de compilación detecta el código que no se puede ejecutar y lo corta. Hay una buena característica que UglifyJS puede hacer esto.

Ahora pasemos a la eliminación de código muerto más avanzada: método de sacudida de árbol .

Sacudida del árbol


Digamos que tenemos una aplicación que usa Lodash . Dudo mucho que alguien esté usando todo el Lodash. Lo más probable es que se exploten varias funciones como get , IsEmpty , unionBy o similares.

Cuando hacemos sacudidas de árbol, queremos que Webpack “agite” los módulos innecesarios y los deseche, y solo tenemos los necesarios. Esto es sacudir el árbol.

Cómo funciona la sacudida de árboles en Webpack


Digamos que tienes un código como este:

 import { a } from './a.js'; console.log(a); 

El código es muy simple: desde algún módulo, importe la variable a y envíela. Pero hay dos variables en este módulo: a y b . No necesitamos la variable b , y queremos eliminarla.

 export const a = 3 export const b = 4 

Cuando llega Webpack, convierte el código de importación en esto:

 var d = require(0); console.log(d["a"]); 

Nuestra import convirtió en require , pero console.log no console.log cambiado.

La dependencia de Webpack se convierte al siguiente código:

 var a = 3; module.exports["a«] = a; /* unused harmony export b */ var b = 4; 


Webpack dejó la exportación de la variable a , y eliminó la exportación de la variable b , pero dejó la variable en sí, marcándola con un comentario especial. En el código convertido, la variable b no se usa y UglifyJS puede eliminarla.

La sacudida del árbol de Webpack solo funciona si tienes algún tipo de minificador de código, como UglifyJS o babel-minify .

Consideremos casos más interesantes, cuando la sacudida del árbol no funciona.

Cuando el movimiento de los árboles no funciona


Caso No. 1. Usted escribe el código:

 module.exports.a = 3; module.exports.b = 4; 

Ejecute el código a través de Webpack, y sigue siendo el mismo. Esto se debe a que el agrupador organiza la sacudida del árbol solo si usa módulos ES6. Si usa módulos CommonJS, la sacudida de árbol no funcionará.

Caso No. 2. Usted escribe código con módulos ES6 y exportaciones con nombre.

 export const a = 3 export const b = 4 

Si su código se ejecuta a través de Babel y no configuró la opción de módulos en falso , entonces Babel traerá sus módulos a CommonJS y Webpack nuevamente no podrá ejecutar Tree Shaking, porque solo funciona con módulos ES6.

 module.exports.a = 3; module.exports.b = 4; 

En consecuencia, debemos asegurarnos de que nadie en nuestro plan de ensamblaje transforme los módulos ES6.

Caso No. 3. Supongamos que tenemos una clase tan inútil que no hace nada: export class ShakeMe {} . Además, todavía no lo usamos. Cuando Webpack pasa por importación y exportación, Babel convertirá la clase en una función, y el agrupador notará que la función no se utiliza:

 /* unused harmony e[port b */ var ShakeMe = function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }(); 

Parece que todo debería estar bien, pero si miramos más de cerca, veremos que dentro de esta función hay una variable global babelHelpers , desde la que se llama a alguna función. Este es un efecto secundario : UglifyJS ve que se está llamando a alguna función global y no cortará el código, porque teme que algo se rompa.

Cuando escribes clases y las ejecutas a través de Babel, nunca se cortan. ¿Cómo se soluciona esto? Hay un hack estandarizado: agregue un comentario /*#__PURE__*/ antes de la función:

 /* unused harmony export b */ var ShakeMe = /*#__PURE__*/ function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }(); 

Entonces UglifyJS creerá en la palabra que la siguiente función es pura. Afortunadamente, Babel 7 está haciendo esto ahora, y en Babel 6, nada se ha eliminado hasta ahora.

Regla: si tienes un efecto secundario en alguna parte, UglifyJS no hará nada.

Para resumir:

  • La sacudida de árboles no funciona para la mayoría de las bibliotecas de npm , porque todas son de CommonJS y están construidas por el antiguo Babel.
  • Lo más probable es que Tree Shaking funcione adecuadamente para aquellas bibliotecas que ya están preparadas para esto , por ejemplo, Lodash-es, Date-fns y su código o bibliotecas.
  • UglifyJS está involucrado en la asamblea.
  • Módulos ES6 usados.
  • Sin efectos secundarios.

Descubrimos cómo reducir el peso del paquete y ahora vamos a enseñarle a cargar solo la funcionalidad necesaria.

Solo cargamos la funcionalidad necesaria


Dividimos esta parte en dos. En la primera parte, solo se carga el código que requiere el usuario : si el usuario visita la página principal de su sitio, no carga las páginas de la cuenta personal. En el segundo, los cambios en el código conducen a la recarga de recursos más pequeña posible .

Solo cargamos el código necesario


Considere la estructura de una aplicación imaginaria. Tiene:

  • Punto de entrada - APP.
  • Tres páginas: inicio, búsqueda y tarjeta.

imagen

El primer problema que queremos resolver es emitir un código común . Denotemos el código rojo como el código común para todas las páginas, el círculo verde para la página principal y la página de búsqueda. Las cifras restantes no son particularmente importantes.

imagen

Cuando el usuario realiza la búsqueda desde la página principal, volverá a cargar el cuadro y el círculo por segunda vez, aunque ya los tiene. Idealmente, nos gustaría ver algo como esto.

imagen

Es bueno que Webpack 4 ya tenga un complemento incorporado que lo haga por nosotros: SplitChunksPlugin . El complemento extrae el código de la aplicación o el código de los módulos de nodo, que es utilizado por varios fragmentos en un fragmento separado, al tiempo que garantiza que el fragmento con el código común tendrá más de 30 Kb, y para cargar la página no necesita descargar más de 5 fragmentos. La estrategia es óptima: cargar fragmentos demasiado pequeños no es rentable, y cargar demasiados fragmentos es largo y no es tan eficiente como descargar menos fragmentos incluso en http2. Para repetir este comportamiento en 2 o 3 versiones de Webpack, tuve que escribir 20-30 líneas con características no documentadas. Ahora esto se está resolviendo en una línea.

Para llevar CSS


Sería genial si aún elimináramos el CSS para cada fragmento en un archivo separado. Hay una solución lista para esto: Mini-Css-Extract-Plugin . El complemento apareció solo en Webpack 4, y antes no había soluciones adecuadas para tal tarea: solo hacks, dolor y piernas cortadas. El complemento elimina CSS de fragmentos asincrónicos y fue creado específicamente para esta tarea , que se realiza perfectamente.

Recarga de recursos mínima posible


Descubriremos cómo asegurarnos de que, al lanzar, por ejemplo, un nuevo bloque de promoción en la página principal, el usuario vuelva a cargar la parte más pequeña posible del código .

Si tuviéramos versiones, todo estaría bien. Aquí tenemos la página principal de la versión N, y después del lanzamiento del bloque promocional - versión N + 1. Webpack proporciona un mecanismo similar desde el primer momento utilizando hashing. Después de que Webpack recopila todos los activos, en este caso app.js, calculará su hash de contenido y lo agregará al nombre del archivo para obtener la aplicación. [Hash] .js. Esta es la versión que necesitamos.

imagen

Veamos cómo funciona. Active los hash, realice cambios en la página principal y vea si el código de la página principal realmente ha cambiado. Veremos que dos archivos han cambiado: main y app.js.

imagen

¿Por qué sucedió esto, porque es ilógico? Para entender por qué, echemos un vistazo a app.js. Consta de tres partes:

  • código de aplicación
  • tiempo de ejecución de paquete web;
  • enlaces a fragmentos asincrónicos.

Cuando cambiamos el código en main, su contenido y hash cambian, lo que significa que el enlace también cambia en la aplicación. La aplicación en sí también cambiará y debe reiniciarse. La solución a este problema es dividir app.js en dos fragmentos: código de aplicación y tiempo de ejecución de paquete web y enlaces a fragmentos asincrónicos. Webpack 4 hace todo por nosotros con una opción runtimeChunk , que pesa muy poco, menos de 2 KB en gzip. Reiniciarlo para el usuario es prácticamente inútil. RuntimeChunk está habilitado con solo una opción:

 optimization: { runtimeChunk: true } 

En Webpack 3 y 2, escribiríamos 5-6 líneas, en lugar de una. Esto no es mucho más, pero sigue siendo un inconveniente innecesario.

imagen

¡Todo es genial, aprendimos a hacer enlaces y tiempo de ejecución! Vamos a escribir un nuevo módulo en main, liberarlo y - ¡op! - ahora, en general, todo se reinicia.

imagen

Por qué Veamos cómo funcionan los módulos en webpack.

Módulos de paquete web


Supongamos que hay un código en el que agrega los módulos a , b , d y e :

 import a from 'a'; import b from 'b'; import d from 'd'; import e from 'e'; 

Webpack convierte las importaciones a require: a , b , dye se reemplazan por require (0), require (1), require (2) y require (3).

 var a = require(0); var b = require(1); var d = require(2); var e = require(3); 

Imagine una imagen que ocurre con mucha frecuencia: escribe un nuevo módulo c import c from 'c'; y pegarlo en algún lugar en el medio:

 import a from 'a'; import b from 'b'; import c from 'c'; import d from 'd'; import e from 'e'; 

Cuando Webpack procesa todo, convierte la importación del nuevo módulo en require (2):

 var a = require(0); var b = require(1); var c = require(2); var d = require(3); var e = require(4); 

Los módulos dye , que eran 2 y 3, recibirán los números 3 y 4, la nueva identificación. De esto se deduce una conclusión simple: usar números de serie como id es un poco tonto, pero Webpack lo hace.

No use el número de serie como identificación única

Para solucionar el problema, hay una solución Webpack incorporada : HashedModuleIdsPlugin :

 new webpack.HashedModuleIdsPlugin({ hashFunction: 'md4′, hashDigest:'base64′, hashDigestLength: 4, }), 

Este complemento utiliza 4 caracteres de hash md4 en lugar de la identificación digital de la ruta absoluta al archivo. Con él, nuestro requerimiento se convertirá en estos:

 var a = require('YmRl'); var b = require('N2Fl'); var c = require('OWE4′); var d = require('NWQz'); var e = require('YWVj'); 

En lugar de números, aparecieron letras. Por supuesto, hay un problema oculto: esta es una colisión de hashes . Nos topamos con él una vez y podemos aconsejarle que use 8 caracteres en lugar de 4. Una vez configurados los hashes correctamente, todo funcionará de la manera que originalmente queríamos.

Ahora sabemos cómo recolectar paquetes de sueños.

  • Minify
  • Usar división de código .
  • Configurar hashes .

Aprendimos a recolectar, y ahora trabajaremos en velocidad.

¿Cómo armar un paquete de sueños rápidamente ?


En nuestro N1.RU, la aplicación más grande consta de 10,000 módulos, y sin optimizaciones, toma 28 minutos. ¡Pudimos acelerar el montaje a dos minutos! ¿Cómo hicimos eso? Hay 3 formas de acelerar cualquier cálculo, y las tres son aplicables a Webpack.

Paralelización de ensamblajes


Lo primero que hicimos fue paralelizar la asamblea . Para esto tenemos:

  • HappyPackPlugin , que envuelve sus cargadores en otros cargadores, y toma todos los cálculos que se envuelven en procesos separados. Esto permite, por ejemplo, paralelizar Babel y node-sass.
  • cargador de hilos . Realiza aproximadamente lo mismo que HappyPackPlugin, solo usa no procesos, sino grupo de subprocesos. Cambiar a un hilo separado es una operación costosa, úsela con cuidado y solo si desea envolver operaciones pesadas y que requieren muchos recursos, como babel o node-sass. Para cargar json, por ejemplo, no es necesaria la paralelización, ya que se carga rápidamente.
  • Es muy probable que los complementos y cargadores que usa ya tengan herramientas de paralelización integradas, solo tiene que buscar. Por ejemplo, esta opción está en UglifyJS .

Resultados de compilación de caché


El almacenamiento en caché de los resultados del ensamblaje es la forma más eficiente de acelerar el ensamblaje de Webpack.

La primera solución que tenemos es el cargador de caché . Este es un cargador que se introduce en una cadena de cargadores y guarda el resultado de crear un archivo específico para una cadena específica de cargadores en el sistema de archivos. En el siguiente ensamblaje del paquete, si este archivo está en el sistema de archivos y ya se ha procesado con esta cadena, el cargador de caché tomará los resultados y no llamará a los cargadores que están detrás de ellos, por ejemplo, Babel-loader o node-sass.

El gráfico muestra el tiempo de montaje. Barra azul: 100% de tiempo de compilación, sin cargador de caché, y con ella: 7% más lento. Esto se debe a que el cargador de caché pasa más tiempo guardando cachés en el sistema de archivos. Ya en la segunda asamblea, recibimos una ganancia tangible: la asamblea fue 2 veces más rápida.

imagen

La segunda solución es más sofisticada : HardSourcePlugin . La principal diferencia: el cargador de caché es solo un cargador que puede operar solo en una cadena de cargadores con código o archivos, y HardSourcePlugin tiene acceso casi completo al ecosistema Webpack, puede operar con otros complementos y cargadores, y extiende el ecosistema para el almacenamiento en caché un poco. El gráfico anterior muestra que en el primer lanzamiento, el tiempo de construcción aumentó en un 37%, pero en el segundo lanzamiento con todos los cachés, aceleramos 5 veces.

imagen

La mejor parte es que puedes usar ambas soluciones juntas, que es lo que hacemos en N1.RU. Tenga cuidado, porque hay problemas con los cachés, de los que hablaré más adelante.

Los complementos / cargadores que ya usa pueden tener mecanismos de almacenamiento en caché integrados . Por ejemplo, babel-loader tiene un sistema de almacenamiento en caché muy eficiente, pero por alguna razón está desactivado de forma predeterminada. La misma funcionalidad se encuentra en el increíble cargador de script de tipo . El complemento UglifyJS también tiene almacenamiento en caché, que funciona muy bien. Nos aceleró por varios minutos.

Y ahora los problemas.

Problemas de almacenamiento en caché


  • Es posible que el caché no se valide correctamente .
  • Las soluciones aplicadas pueden no funcionar con complementos conectados, cargadores, su código o entre sí . En este sentido, el cargador de caché es una solución simple y sin problemas. Pero con HardSourcePlugin debes ser más cuidadoso.
  • Es difícil debutar si todo está roto . Cuando el almacenamiento en caché no funciona correctamente y se produce un error incomprensible, será muy difícil descubrir cuál es el problema.

¿Cómo ahorrar en producción?


La última forma de acelerar un proceso es no hacer ninguna parte del proceso. Pensemos en cómo puede ahorrar en producción. ¿Qué no podemos hacer? La respuesta es breve: ¡ no podemos hacer nada ! No tenemos derecho a rechazar algo en producción, pero podemos ahorrar mucho en desarrollo.

En qué ahorrar:

  • No recopile el mapa fuente hasta que los necesitemos.
  • Utilice el cargador de estilo en lugar de un esquema genial con eliminación y procesamiento de CSS a través de cargadores de CSS. El cargador de estilo en sí mismo es muy rápido, porque toma la línea css y la inserta en una función que inserta esa línea en la etiqueta de estilo.
  • Puede dejar en la lista del navegador solo el navegador que usa específicamente; lo más probable es que este sea el último Chrome . Esto se acelerará mucho .
  • Abandone completamente cualquier optimización de recursos : desde UglifyJS, css-nano, gzip / brotli.

La aceleración de compilación es paralelización, almacenamiento en caché y rechazo de cálculos. Siguiendo estos tres simples pasos, puede acelerar mucho.

¿Cómo configurar webpack?


Descubrimos cómo ensamblar un paquete de sueños y cómo ensamblarlo rápidamente, y ahora descubriremos cómo configurar Webpack para que no nos peguemos un tiro en la pierna cada vez que cambie la configuración.

Evolución de la configuración en el proyecto.


Una ruta típica de webpack-config en un proyecto comienza con una configuración simple . Al principio solo inserta Webpack, Babel-loader, sass-loader y todo está bien. Luego, inesperadamente, aparecen algunas condiciones en process.env e inserta las condiciones. Uno, segundo, tercero, más y más, hasta que se agregue una condición con una opción "mágica". Entiende que todo ya está bastante mal, y es mejor simplemente duplicar las configuraciones para desarrollo y producción, y hacer correcciones dos veces. Todo será más claro. Si tuvo un pensamiento: "¿Hay algo mal aquí?", Entonces el único consejo práctico es mantener la configuración en orden . Te diré cómo lo hacemos.

Mantenga la configuración en orden


Usamos el paquete webpack-merge . Este es un paquete npm creado para combinar varias configuraciones en una sola. Si no se siente cómodo con la estrategia de fusión predeterminada, puede personalizarla.


4 :

  • Loaders.
  • Plugins.
  • Presets.
  • Parts.

.

Plugin/Loader


, , API, , .

Se parece a esto:

 /** *  JSdoc * @param {Object} options * @see    */ module.exports = function createPlugin(options) { return new Plugin(options); }; 

, , , . , url-loader :

 /** * url-loader    file-loader.        * * @example * -   some-image.png.     url-loader,  url-loader    * 1.    ,  url-loader    base64  * 2. , url-loader    outputPath + name     ,     . *    some-image.png,     outputPath/images/some-image.12345678hash.png,  url-loader  * publicPath/images/some-image.12345678hash.png * * @param {string} prefix    * @param {number} limit    ,    * @return {Object} loader   * @see https://www.npmjs.com/package/url-loader */ 

, , , , , , . , , , , url-loader. :

 function urlLoader(prefix = 'assets', limit = 100) { return { loader: 'url-loader', options: { limit, name: `${prefix}/[name].[hash].[ext]` } }; }; 

. , Loader .

Preset


webpack. , , , webpack, . — , , scss-:

 { test: /\.scss$/, use: [cssLoader, postCssLoader, scssLoader] } 

.

Part


— , . , , . , :

 entry: { app: './src/Frontend/app.js' }, output: { publicPath: '/static/cabinet/app/', path: path.resolve('www/static/app') }, 

:

  • , , , json, , , splitChunks.
  • dev , , js/css
  • Part , output, publicPath, entry-point , , source map.

imagen

Webpack-merge . , . webpack-merge 3-7 , Babel-loader, . , .


Para resumir. , . , webpack — . , .

, !

Frontend Conf . , — , , Frontend Conf ++ .

- ? FrontenConf ++ , 27 28 . 27 , 15 . — !

Source: https://habr.com/ru/post/es433324/


All Articles