Cómo usar webpack para que tu app sea lo más pequeña posible
Una de las primeras cosas que debes hacer cuando optimizas una aplicación es hacerla lo más pequeña posible. A continuación, te indicamos cómo hacerlo con webpack.
Usa el modo de producción (solo webpack 4)
Webpack 4 introdujo la nueva marca mode
. Puedes establecer esta marca en 'development'
o 'production'
para indicarle a Webpack que estás compilando la aplicación para un entorno específico:
// webpack.config.js
module.exports = {
mode: 'production',
};
Asegúrate de habilitar el modo production
cuando compiles tu app para producción.
Esto hará que webpack aplique optimizaciones como la reducción, la eliminación de código solo para desarrollo en bibliotecas y mucho más.
Lecturas adicionales
Habilita la reducción
La reducción se produce cuando comprimes el código quitando espacios adicionales, acortando los nombres de las variables, etcétera. Para ello, puedes escribir lo siguiente:
// 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 admite dos formas de reducir el código: la reducción a nivel del paquete y las opciones específicas del cargador. Se deben usar de forma simultánea.
Reducción a nivel del paquete
La reducción a nivel del paquete comprime todo el paquete después de la compilación. Funciona de la siguiente manera:
Escribes el código de la siguiente manera:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack lo compila en aproximadamente lo siguiente:
// 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 minificador lo comprime en aproximadamente lo siguiente:
// 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)
En webpack 4, la reducción a nivel del paquete se habilita automáticamente, tanto en el modo de producción como sin uno. En segundo plano, usa el minificador UglifyJS. (Si alguna vez necesitas inhabilitar la reducción, usa el modo de desarrollo o pasa false
a la opción optimization.minimize
).
En webpack 3, debes usar el complemento UglifyJS directamente. El complemento viene incluido con webpack. Para habilitarlo, agrégalo a la sección plugins
de la configuración:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
Opciones específicas del cargador
La segunda forma de reducir el código es con opciones específicas del cargador (qué es un cargador). Con las opciones del cargador, puedes comprimir elementos que el minificador no puede reducir. Por ejemplo, cuando importas un archivo CSS con css-loader
, el archivo se compila en una cadena:
/* 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}",""]);
El minimizador no puede comprimir este código porque es una cadena. Para reducir el contenido del archivo, debemos configurar el cargador para que haga lo siguiente:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Lecturas adicionales
- Documentos de UglifyJsPlugin
- Otros minificadores populares: Babel Minify y Google Closure Compiler
Especifica NODE_ENV=production
Otra forma de disminuir el tamaño del frontend es establecer la variable de entorno NODE_ENV
en tu código en el valor production
.
Las bibliotecas leen la variable NODE_ENV
para detectar en qué modo deben funcionar: en el modo de desarrollo o en el de producción. Algunas bibliotecas se comportan de forma diferente según esta variable. Por ejemplo, cuando NODE_ENV
no está configurado en production
, Vue.js realiza verificaciones adicionales y muestra advertencias:
// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.');
}
// …
React funciona de manera similar: carga una compilación de desarrollo que incluye las advertencias:
// 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.'
);
// …
Por lo general, esas verificaciones y advertencias no son necesarias en producción, pero permanecen en el código y aumentan el tamaño de la biblioteca. En webpack 4, quítalos agregando la opción optimization.nodeEnv: 'production'
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
En webpack 3, usa DefinePlugin
en su lugar:
// 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()
]
};
Tanto la opción optimization.nodeEnv
como DefinePlugin
funcionan de la misma manera: reemplazan todas las instancias de process.env.NODE_ENV
por el valor especificado. Con la configuración anterior, sucede lo siguiente:
Webpack reemplazará todas las instancias de
process.env.NODE_ENV
por"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.'); }
Luego, el minificador quitará todas esas ramas
if
, ya que"production" !== 'production'
siempre es falso y el complemento comprende que el código dentro de estas ramas nunca se ejecutará:// 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 }; }
Lecturas adicionales
- Qué son las “variables de entorno”
- Documentación de Webpack sobre
DefinePlugin
yEnvironmentPlugin
Usa módulos de ES
La siguiente forma de disminuir el tamaño del frontend es usar módulos de ES.
Cuando usas módulos ES, webpack puede realizar el proceso de eliminación de árboles. El árbol de sacudidas ocurre cuando un empaquetador traversea todo el árbol de dependencias, verifica qué dependencias se usan y quita las que no se usan. Por lo tanto, si usas la sintaxis de módulos ES, webpack puede eliminar el código sin usar:
Escribes un archivo con varias exportaciones, pero la app solo usa una de ellas:
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
Webpack comprende que no se usa
commentRestEndpoint
y no genera un punto de exportación independiente en el paquete:// 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 */ })
El minimizador quita la variable que no se usa:
// bundle.js (part that corresponds to comments.js) (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
Esto funciona incluso con bibliotecas si están escritas con módulos ES.
Sin embargo, no es necesario que uses precisamente el minificador integrado de webpack (UglifyJsPlugin
).
Cualquier minificador que admita la eliminación de código muerto (p.ej., el complemento Babel Minify o el complemento Google Closure Compiler) hará el truco.
Lecturas adicionales
Documentación de Webpack sobre la eliminación de código no utilizado
Optimiza imágenes
Las imágenes representan más de la mitad del tamaño de la página. Si bien no son tan importantes como JavaScript (p.ej., no bloquean la renderización), aún consumen una gran parte del ancho de banda. Usa url-loader
, svg-url-loader
y image-webpack-loader
para optimizarlos en webpack.
url-loader
intercala pequeños archivos estáticos en la app. Sin configuración, toma un archivo pasado, lo coloca junto al paquete compilado y muestra una URL de ese archivo. Sin embargo, si especificamos la opción limit
, se codificarán los archivos más pequeños que este límite como una URL de datos de Base64 y se mostrará esta URL. Esto intercala la imagen en el código JavaScript y guarda una solicitud HTTP:
// 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
funciona igual que url-loader
, excepto que codifica los archivos con la codificación de URL en lugar de la de Base64. Esto es útil para las imágenes SVG, ya que los archivos SVG son solo texto sin formato, por lo que esta codificación es más eficiente en términos de tamaño.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
comprime las imágenes que pasan por él. Admite imágenes JPG, PNG, GIF y SVG, por lo que la usaremos para todos estos tipos.
Este cargador no incorpora imágenes en la app, por lo que debe funcionar en conjunto con url-loader
y svg-url-loader
. Para evitar copiarlo y pegarlo en ambas reglas (una para imágenes JPG/PNG/GIF y otra para imágenes SVG), incluiremos este cargador como una regla independiente con 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'
}
]
}
};
La configuración predeterminada del cargador ya está lista, pero si quieres configurarla más, consulta las opciones del complemento. Para elegir qué opciones especificar, consulta la excelente guía sobre la optimización de imágenes de Addy Osmani.
Lecturas adicionales
Optimiza las dependencias
Más de la mitad del tamaño promedio de JavaScript proviene de dependencias, y es posible que una parte de ese tamaño sea innecesario.
Por ejemplo, Lodash (a partir de la versión 4.17.4) agrega 72 KB de código reducido al paquete. Sin embargo, si solo usas, por ejemplo, 20 de sus métodos, aproximadamente 65 KB de código reducido no hacen nada.
Otro ejemplo es Moment.js. Su versión 2.19.1 ocupa 223 KB de código reducido, lo que es enorme. El tamaño promedio de JavaScript en una página fue de 452 KB en octubre de 2017. Sin embargo, 170 KB de ese tamaño corresponden a archivos de localización. Si no usas Moment.js con varios idiomas, estos archivos aumentarán el tamaño del paquete sin ningún propósito.
Todas estas dependencias se pueden optimizar fácilmente. Recopilamos enfoques de optimización en un repositorio de GitHub. ¡Échalo un vistazo!
Habilita la concatenación de módulos para los módulos de ES (también conocida como elevación de alcance)
Cuando compilas un paquete, webpack une cada módulo en una función:
// 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!');
}
})
En el pasado, esto era necesario para aislar los módulos de CommonJS/AMD entre sí. Sin embargo, esto agregó una sobrecarga de tamaño y rendimiento para cada módulo.
Webpack 2 introdujo la compatibilidad con módulos ES que, a diferencia de los módulos CommonJS y AMD, se pueden agrupar sin unir cada uno con una función. Y webpack 3 hizo posible ese empaquetado con la concatenación de módulos. Esto es lo que hace la concatenación de módulos:
// 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();
})
¿Ves la diferencia? En el paquete simple, el módulo 0 necesitaba render
del módulo 1. Con la concatenación de módulos, require
simplemente se reemplaza por la función requerida y se quita el módulo 1. El paquete tiene menos módulos y menos sobrecarga de módulos.
Para activar este comportamiento, en webpack 4, habilita la opción optimization.concatenateModules
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true
}
};
En webpack 3, usa ModuleConcatenationPlugin
:
// webpack.config.js (for webpack 3)
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
Lecturas adicionales
- Documentación de Webpack para el módulo ModuleConcatenationPlugin
- “Introducción breve al ascenso de alcance”
- Descripción detallada de lo que hace este complemento
Usa externals
si tienes código de webpack y no de webpack.
Es posible que tengas un proyecto grande en el que parte del código se compila con webpack y otra parte no. Como un sitio de alojamiento de videos, en el que el widget del reproductor se puede compilar con webpack, pero la página circundante no:
Si ambos fragmentos de código tienen dependencias comunes, puedes compartirlos para evitar descargar su código varias veces. Esto se hace con la opción externals
de webpack, que reemplaza los módulos por variables o por otras importaciones externas.
Si las dependencias están disponibles en window
Si tu código que no es de Webpack depende de dependencias que están disponibles como variables en window
, asigna alias a los nombres de las dependencias a los nombres de las variables:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
Con esta configuración, webpack no agrupará los paquetes react
y react-dom
. En su lugar, se reemplazarán por algo como lo siguiente:
// 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 las dependencias se cargan como paquetes AMD
Si tu código que no es de webpack no expone dependencias en window
, la situación se complica.
Sin embargo, puedes evitar cargar el mismo código dos veces si el código que no es de Webpack consume estas dependencias como paquetes AMD.
Para ello, compila el código de webpack como un paquete AMD y crea alias de módulos a las URLs de la biblioteca:
// webpack.config.js
module.exports = {
output: {
libraryTarget: 'amd'
},
externals: {
'react': {
amd: '/libraries/react.min.js'
},
'react-dom': {
amd: '/libraries/react-dom.min.js'
}
}
};
Webpack unirá el paquete en define()
y hará que dependa de estas URLs:
// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
Si el código que no es de Webpack usa las mismas URLs para cargar sus dependencias, estos archivos se cargarán solo una vez. Las solicitudes adicionales usarán la caché del cargador.
Lecturas adicionales
- Documentos de Webpack en
externals
En resumen
- Habilita el modo de producción si usas webpack 4.
- Minimiza tu código con las opciones de minimizador y cargador a nivel del paquete
- Para quitar el código solo para desarrollo, reemplaza
NODE_ENV
porproduction
. - Usa módulos de ES para habilitar el movimiento de árboles
- Comprime imágenes
- Aplica optimizaciones específicas de la dependencia
- Habilita la concatenación de módulos
- Usa
externals
si te resulta conveniente.