Cómo Webpack ayuda con el almacenamiento en caché de recursos
El siguiente paso (después de optimizar el tamaño de la app que mejora el tiempo de carga de la app es el almacenamiento en caché). Úsalo para mantener partes de la app en el cliente y evitar volver a descargarlas cada vez.
Usa el control de versiones de paquetes y los encabezados de caché
El enfoque común para hacer el almacenamiento en caché es el siguiente:
Indica al navegador que almacene en caché un archivo durante mucho tiempo (por ejemplo, un año):
# Server header Cache-Control: max-age=31536000
Si no sabes qué hace
Cache-Control
, consulta la excelente publicación de Jake Archibald sobre las prácticas recomendadas de almacenamiento en caché.y cambiar el nombre del archivo cuando se modifique para forzar la nueva descarga:
<!-- Before the change --> <script src="./index-v15.js"></script> <!-- After the change --> <script src="./index-v16.js"></script>
Este enfoque le indica al navegador que descargue el archivo JS, lo almacene en la caché y use la copia almacenada en caché. El navegador solo accederá a la red si cambia el nombre del archivo (o si pasa un año).
Con webpack haces lo mismo, pero en lugar de un número de versión, especificas el hash del archivo. Para incluir el hash en el nombre del archivo, usa [chunkhash]
:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
}
};
Si necesitas el nombre del archivo para enviarlo al cliente, usa HtmlWebpackPlugin
o WebpackManifestPlugin
.
HtmlWebpackPlugin
es un enfoque simple, pero menos flexible. Durante la compilación, este complemento genera un archivo HTML que incluye todos los recursos compilados. Si la lógica del servidor no es compleja, debería ser suficiente por ti:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
es un enfoque más flexible que resulta útil si tienes una parte compleja del servidor.
Durante la compilación, genera un archivo JSON con una asignación entre los nombres de archivos sin hash y los nombres de archivos con hash. Usa este JSON en el servidor para averiguar con qué archivo trabajar:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
Lecturas adicionales
- Jake Archibald sobre prácticas recomendadas de almacenamiento en caché
Extrae las dependencias y el entorno de ejecución en un archivo separado.
Dependencias
Las dependencias de la app tienden a cambiar con menos frecuencia que el código real de la app. Si los mueves a un archivo separado, el navegador podrá almacenarlos en caché por separado y no los volverá a descargar cada vez que cambie el código de la app.
Para extraer las dependencias en un bloque separado, realiza tres pasos:
Reemplaza el nombre del archivo de salida por
[name].[chunkname].js
:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };
Cuando webpack compila la app, reemplaza
[name]
por el nombre de un fragmento. Si no agregamos la parte[name]
, tendremos que diferenciar los fragmentos por su hash, lo que es bastante difícil.Convierte el campo
entry
en un objeto:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
En este fragmento, "main" es el nombre de un fragmento. Este nombre se reemplazará en lugar de
[name]
del paso 1.Por ahora, si compilas la app, este fragmento incluirá todo el código de la app, de la misma manera que no completamos estos pasos. Pero esto cambiará en un segundo.
En webpack 4, agrega la opción
optimization.splitChunks.chunks: 'all'
a la configuración de tu webpack:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Esta opción habilita la división de código inteligente. Con él, webpack extraerá el código del proveedor si supera los 30 KB (antes de la reducción y gzip). También extraerá el código común, lo que resulta útil si tu compilación produce varios paquetes (p.ej., si divides tu app en rutas).
En webpack 3, agrega lo siguiente
CommonsChunkPlugin
:// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ // A name of the chunk that will include the dependencies. // This name is substituted in place of [name] from step 1 name: 'vendor', // A function that determines which modules to include into this chunk minChunks: module => module.context && module.context.includes('node_modules'), }) ] };
Este complemento toma todos los módulos cuyas rutas de acceso incluyen
node_modules
y los mueve a un archivo separado llamadovendor.[chunkhash].js
.
Después de estos cambios, cada compilación generará dos archivos en lugar de uno: main.[chunkhash].js
y vendor.[chunkhash].js
(vendors~main.[chunkhash].js
para webpack 4). En el caso de webpack 4, es posible que el paquete del proveedor no se genere si las dependencias son pequeñas, y eso está bien:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
El navegador almacenaría en caché estos archivos por separado y volvería a descargar solo el código que cambia.
Código de entorno de ejecución de Webpack
Por desgracia, no basta con extraer solo el código del proveedor. Si intentas cambiar algo en el código de la app, haz lo siguiente:
// index.js
…
…
// E.g. add this:
console.log('Wat');
Verás que el hash vendor
también cambia:
Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
↓
Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
Esto sucede porque el paquete webpack, además del código de los módulos, tiene un entorno de ejecución, es decir, un pequeño fragmento de código que administra la ejecución del módulo. Cuando divides el código en varios archivos, este fragmento comienza a incluir una asignación entre los IDs de fragmento y los archivos correspondientes:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack incluye este entorno de ejecución en el último fragmento generado, que es vendor
en nuestro caso. Cada vez que se modifica un fragmento, este fragmento del código también cambia, lo que hace que cambie todo el fragmento vendor
.
Para resolver esto, muevamos el entorno de ejecución a un archivo separado. En webpack 4, esto se logra habilitando la opción optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
En webpack 3, crea un fragmento vacío adicional con el CommonsChunkPlugin
:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => module.context && module.context.includes('node_modules')
}),
// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity
})
]
};
Después de estos cambios, cada compilación generará tres archivos:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
Inclúyelas en index.html
en orden inverso, y listo:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
Lecturas adicionales
- Guía de webhook sobre el almacenamiento en caché a largo plazo
- Documentación de webhook sobre el entorno de ejecución y el manifiesto de webpack
- “Cómo aprovechar al máximo CommonsChunkPlugin”
- Cómo funcionan
optimization.splitChunks
yoptimization.runtimeChunk
Entorno de ejecución de webpack intercalado para guardar una solicitud HTTP adicional
Para mejorar aún más, intenta integrar el entorno de ejecución de webpack en la respuesta HTML. Es decir, en lugar de esto:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
hacer esto:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
El entorno de ejecución es pequeño, y su intercalado te ayudará a guardar una solicitud HTTP (bastante importante con HTTP/1; menos importante con HTTP/2, pero podría tener un efecto de todos modos).
o crear a partir de ellos. Te mostramos cómo.
Si generas HTML con el complemento HtmlWebpackPlugin
Si usas HtmlWebpackPlugin para generar un archivo HTML, solo necesitas InlineSourcePlugin:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
inlineSource: 'runtime~.+\\.js',
}),
new InlineSourcePlugin()
]
};
Si generas HTML mediante una lógica de servidor personalizada
Con webpack 4:
Agrega
WebpackManifestPlugin
para conocer el nombre generado del bloque del entorno de ejecución:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };
Una compilación con este complemento creará un archivo similar al siguiente:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
Intercala el contenido del fragmento de entorno de ejecución de forma conveniente. P.ej., con Node.js y Express:
// server.js const fs = require('fs'); const manifest = require('./manifest.json'); const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
O con webpack 3:
Especifica
filename
para hacer que el nombre del entorno de ejecución sea estático:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Intercala el contenido de
runtime.js
de manera conveniente. P.ej., con Node.js y Express:// server.js const fs = require('fs'); const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Código de carga diferida que no necesitas en este momento
A veces, una página tiene más y menos partes importantes:
- Si cargas la página de un video en YouTube, te interesa más el video que los comentarios. Aquí, el video es más importante que los comentarios.
- Si abres un artículo en un sitio de noticias, te interesa más el texto del artículo que los anuncios. Aquí, el texto es más importante que los anuncios.
En esos casos, para mejorar el rendimiento de carga inicial, descarga solo los elementos más importantes primero y, luego, realiza una carga diferida de las partes restantes. Usa la función import()
y la división de código para lo siguiente:
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
import()
especifica que quieres cargar un módulo específico de forma dinámica. Cuando webpack ve import('./module.js')
, mueve este módulo a un fragmento independiente:
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
y lo descarga solo cuando la ejecución alcanza la función import()
.
De esta manera, se reducirá el paquete main
y se mejorará el tiempo de carga inicial.
Aun más, mejorará el almacenamiento en caché. Si cambias el código en el bloque principal, el bloque de comentarios no se verá afectado.
Lecturas adicionales
- Documentación de webhook para la función
import()
- La propuesta de JavaScript para implementar la sintaxis
import()
Divide el código en rutas y páginas
Si tu app tiene varias rutas o páginas, pero solo hay un archivo JS con el código (un solo fragmento main
), es probable que estés entregando bytes adicionales en cada solicitud. Por ejemplo, cuando un usuario visita una página principal de tu sitio:
no necesita cargar el código para renderizar un artículo que está en una página diferente, pero lo cargará. Además, si el usuario siempre visita solo la página principal y realizas un cambio en el código del artículo, webpack invalidará todo el paquete, y el usuario deberá volver a descargar la app completa.
Si dividimos la app en páginas (o rutas, si es de una sola página), el usuario solo descargará el código relevante. Además, el navegador almacenará mejor en caché el código de la app: si cambias el código de la página principal, webpack invalidará solo el fragmento correspondiente.
Apps de una sola página
Para dividir las apps de una sola página por rutas, usa import()
(consulta la sección "Código de carga diferida que no necesitas en este momento"). Si usas un framework, es posible que tenga una solución existente para esto:
- "División de código" en los documentos de
react-router
(para React) - "Rutas de carga diferida" en los documentos de
vue-router
(para Vue.js)
Para apps tradicionales de varias páginas
Para dividir las apps tradicionales por páginas, usa los puntos de entrada de webpack. Si tu app tiene tres tipos de páginas: la página principal, la del artículo y la de la cuenta de usuario, debería tener tres entradas:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Para cada archivo de entrada, webpack compilará un árbol de dependencias independiente y generará un paquete que incluya solo los módulos que usa esa entrada:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
Por lo tanto, si solo la página del artículo usa Lodash, los paquetes home
y profile
no lo incluirán, y el usuario no tendrá que descargar esta biblioteca cuando visite la página principal.
Sin embargo, los árboles de dependencia separados tienen sus desventajas. Si dos puntos de entrada usan Lodash y no moviste tus dependencias a un paquete de proveedores, ambos puntos de entrada incluirán una copia de Lodash. Para resolver esto, en webpack 4,agrega la opción optimization.splitChunks.chunks: 'all'
a la configuración de tu paquete web:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Esta opción habilita la división de código inteligente. Con esta opción, webpack buscará automáticamente el código común y lo extraerá en archivos separados.
O bien, en webpack 3, usa CommonsChunkPlugin
, que moverá las dependencias comunes a un archivo especificado nuevo:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Juega con el valor de minChunks
para encontrar el mejor. En general, se recomienda que sea pequeño, pero aumentarlo si la cantidad de fragmentos aumenta. Por ejemplo, para 3 fragmentos, minChunks
podría ser 2, pero para 30, podría ser 8. Esto se debe a que, si se mantiene en 2, se ingresarán demasiados módulos en el archivo común y se infla demasiado.
Lecturas adicionales
- Documentos de webpack sobre el concepto de puntos de entrada
- Documentación sobre Webpack sobre el complemento CommonsChunkPlugin
- “Cómo aprovechar al máximo CommonsChunkPlugin”
- Cómo funcionan
optimization.splitChunks
yoptimization.runtimeChunk
Cómo mejorar la estabilidad de los ID de módulo
Cuando se compila el código, webpack asigna un ID a cada módulo. Más adelante, estos IDs se
usan en los objetos require()
dentro del paquete. Por lo general, ves IDs en el resultado de la compilación justo antes de las rutas de acceso de los módulos:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
↓ Aquí
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
De forma predeterminada, los IDs se calculan con un contador (es decir, el primer módulo tiene un ID 0, el segundo tiene un ID 1, y así sucesivamente). El problema es que, cuando agregas un módulo nuevo, es posible que este aparezca en el medio de la lista de módulos y cambie todos los IDs de los siguientes módulos:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
↓ Agregamos un módulo nuevo...
[4] ./webPlayer.js 24 kB {1} [built]
↓ Mira lo que logró. comments.js
ahora tiene el ID 5 en lugar del 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
ahora tiene el ID 6 en lugar del 5.
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Esto invalida todos los fragmentos que incluyen módulos con ID modificados o que dependen de ellos, incluso si su código real no ha cambiado. En nuestro caso, se invalidan el fragmento 0
(el fragmento con comments.js
) y el main
(el fragmento con el otro código de la app), mientras que solo debería haber sido el fragmento main
.
Para resolver esto, cambia la manera en que se calculan los IDs de módulos con HashedModuleIdsPlugin
.
Reemplaza los ID basados en contadores con hashes de rutas de acceso de módulos:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
↓ Aquí
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Con este enfoque, el ID de un módulo solo cambia si le cambias el nombre o lo mueves. Los módulos nuevos no afectarán los IDs de otros módulos.
Para habilitar el complemento, agrégalo a la sección plugins
de la configuración:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Lecturas adicionales
- Documentación de Webpack sobre el complemento HashedModuleIdsPlugin
En resumen
- Almacena en caché el paquete y diferencia las versiones cambiando el nombre del paquete
- Divide el paquete en código de la app, código del proveedor y entorno de ejecución
- Intercala el entorno de ejecución para guardar una solicitud HTTP
- Carga diferida de código no crítico con
import
- Divide el código por rutas o páginas para evitar cargar elementos innecesarios.