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.
Cómo usar el modo de producción (solo para Webpack 4)
Webpack 4 introdujo la nueva marca mode
. Puedes establecer esta marca en 'development'
o 'production'
para sugerir 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 del código exclusivo para desarrollo en bibliotecas y mucho más.
Lecturas adicionales
Habilitar la reducción
La reducción ocurre cuando comprimes el código mediante la eliminación de espacios adicionales, la reducción de los nombres de variables, etc. 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. Deben usarse simultáneamente.
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 código como el siguiente:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
Webpack lo compila aproximadamente en 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 en el modo de producción y sin ella. Usa el minificador UglifyJS de forma interna. (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 con un 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 son las 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 minificador no puede comprimir este código porque es una cadena. Para reducir el contenido del archivo, debemos configurar el cargador de esta manera:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
Lecturas adicionales
- Los documentos de UglifyJsPlugin
- Otros minificadores populares: Babel Minify, 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 con el valor production
.
Las bibliotecas leen la variable NODE_ENV
para detectar en qué modo deberían funcionar: en el de desarrollo o en el de producción. Algunas bibliotecas se comportan de manera diferente según esta variable. Por
ejemplo, cuando NODE_ENV
no está configurado como production
, Vue.js realiza verificaciones adicionales e imprime
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.'
);
// …
Estas comprobaciones y advertencias suelen ser innecesarias en la producción, pero permanecen en el código y aumentan el tamaño de la biblioteca. En webpack 4, para quitarlos, agrega 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()
]
};
La opción optimization.nodeEnv
y DefinePlugin
funcionan de la misma manera: reemplazan todos los casos de process.env.NODE_ENV
con el valor especificado. Con la configuración anterior:
Webpack reemplazará todos los casos 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 esas 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”
- Documentos sobre Webpack sobre:
DefinePlugin
yEnvironmentPlugin
Cómo usar módulos de ES
La siguiente forma de disminuir el tamaño del frontend es usar módulos de ES.
Cuando utilizas módulos de ES, webpack puede realizar la eliminación de código no utilizado. La eliminación de código no se produce cuando un agrupador recorre todo el árbol de dependencias, verifica qué dependencias se usan y quita las que no se usan. Por lo tanto, si usas la sintaxis del módulo ES, webpack puede eliminar el código que no se usa:
Escribes un archivo con varias exportaciones, pero la app usa solo 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
commentRestEndpoint
no se usa y no genera un punto de exportación separado 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 minificador 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 se escriben con módulos de ES.
Sin embargo, no es necesario que uses exactamente el minificador integrado de Webpack (UglifyJsPlugin
).
Cualquier minificador que admita la eliminación de código no 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 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 que se pasó, lo coloca junto al paquete compilado y muestra una URL de ese archivo. Sin embargo, si especificamos la opción limit
, codificará los archivos menores que este límite como una URL de datos Base64 y 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 archivos con la codificación URL en lugar de la base64. Esto es útil para imágenes SVG. Debido a que los archivos SVG son solo texto sin formato, esta codificación es más eficaz.
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: "svg-url-loader",
options: {
limit: 10 * 1024,
noquotes: true
}
}
]
}
};
image-webpack-loader
comprime las imágenes que atraviesan. Es compatible con imágenes JPG, PNG, GIF y SVG, así que la usaremos para todos estos tipos.
Este cargador no incorpora imágenes en la app, por lo que debe funcionar en sincronización 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 SVG), incluiremos este cargador como 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 aún más, consulta las opciones del complemento. Para elegir qué opciones especificar, consulta la excelente guía sobre 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 una parte de ese tamaño podría ser innecesaria.
Por ejemplo, Lodash (a partir de la versión 4.17.4) agrega 72 KB de código reducido al paquete. Pero si solo usas 20 de sus métodos, unos 65 KB de código reducido no hacen nada.
Otro ejemplo es Moment.js. Su versión 2.19.1 usa 223 KB de código reducido, lo cual es enorme: el tamaño promedio de JavaScript en una página era de 452 KB en octubre de 2017. Sin embargo, 170 KB de ese tamaño son de archivos de localización. Si no usas Moment.js con varios idiomas, estos archivos sobrepasarán el paquete sin un propósito.
Todas estas dependencias se pueden optimizar con facilidad. Recopilamos enfoques de optimización en un repositorio de GitHub. Revísalo.
Habilita la concatenación de módulos para módulos ES (es decir, la 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!');
}
})
Antes, esto era necesario para aislar los módulos CommonJS/AMD entre sí. Sin embargo, esto agregaba una sobrecarga de tamaño y rendimiento para cada módulo.
Webpack 2 introdujo 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. Además, webpack 3 posibilitó la creación de paquetes, con 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 requería 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 complemento ModuleConcatenation
- "Introducción breve a la elevación del alcance"
- Descripción detallada de lo que hace este complemento
Usa externals
si tienes un código webpack y otro que no es webpack.
Es posible que tengas un proyecto grande en el que una parte del código se compila con webpack y otra parte no. Como en un sitio de hosting de videos, en el que el widget del reproductor se puede compilar con webpack y la página circundante podría no estarlo:
Si ambos fragmentos de código tienen dependencias comunes, puedes compartirlas para evitar descargar su código varias veces. Para ello, usa la opción externals
de webpack: reemplaza los módulos con variables o con otras importaciones externas.
Si las dependencias están disponibles en window
Si el código que no es de webhook se basa en dependencias que están disponibles como variables en window
, asigna un alias a los nombres de dependencias para los nombres de variables:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
Con esta configuración, webpack no empaquetará los paquetes react
ni react-dom
. En cambio, 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 de AMD
Si el código que no es de webhook no expone las dependencias en window
, el proceso será más complejo.
Sin embargo, puedes evitar cargar el mismo código dos veces si el código que no es de webhook consume estas dependencias como paquetes AMD.
Para ello, compila el código del paquete web como un paquete de AMD y asígnales un alias a las URL 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 webhook 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
- Documentación de Webpack en
externals
En resumen
- Habilita el modo de producción si usas webpack 4
- Minimiza el código con las opciones de cargador y minificador a nivel del paquete
- Para quitar el código exclusivo de desarrollo, reemplaza
NODE_ENV
porproduction
. - Cómo usar módulos de ES para habilitar la eliminación de código no utilizado
- Comprime las imágenes.
- Aplica optimizaciones específicas para la dependencia
- Habilitar la concatenación de módulos
- Usa
externals
si esta opción es adecuada para ti.