Cómo webpack ayuda con el almacenamiento en caché de elementos
Lo siguiente (después de optimizar el tamaño de la app) y mejora el tiempo de carga de la app es almacenar en caché. Úsala para mantener partes de la app en el y evitar volver a descargarlos cada vez.
Usa el control de versiones de paquetes y los encabezados de caché
El enfoque común para realizar el almacenamiento en caché es el siguiente:
Indica al navegador que almacene un archivo en caché durante mucho tiempo (por ejemplo, un año):
# Server header Cache-Control: max-age=31536000
Si no conoces lo que hace
Cache-Control
, consulta el de Jake Archibald publicación excelente sobre almacenamiento en caché prácticas recomendadas.y cambia 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 caché y use la copia almacenada en caché. El navegador solo conectará la red si cambia el nombre del archivo. (o si pasa un año).
Con webpack, usted hace lo mismo, pero en lugar de un número de versión, especifica el
hash de 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
nombre de archivo para enviarlo al cliente, usa HtmlWebpackPlugin
o
WebpackManifestPlugin
El HtmlWebpackPlugin
es un
un enfoque sencillo, pero menos flexible. Durante la compilación, este complemento genera un
que incluye todos los recursos compilados. Si la lógica del servidor no es
complejo, entonces debería ser suficiente para ti:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
El
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 archivo 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 el almacenamiento en caché prácticas
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 te mudas en un archivo separado, el navegador puede almacenarlas en caché y no los vuelve a descargar cada vez que cambia el código de la app.
Para extraer dependencias en un fragmento separado, sigue 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 a
[name]
. con el nombre de un bloque. Si no agregamos la parte[name]
, tendremos diferenciar los fragmentos por su hash, ¡lo cual 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 bloque. Este nombre se sustituirá en lugar de
[name]
del paso 1.A esta altura, si compilas la app, este bloque incluirá todo el código de la app; solo como si no hubiéramos realizado estos pasos. Pero esto cambiará en un segundo.
En webpack 4, agrega la opción
optimization.splitChunks.chunks: 'all'
. en la configuración de webpack:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Esta opción permite la división de código inteligente. Con él, webpack extraería el código del proveedor si supera los 30 KB (antes de la reducción y la compresión en gzip). También extraería el código común: Esto es útil si tu compilación produce varios paquetes (p.ej., si divides tu app en rutas).
En webpack 3, agrega
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 que incluyen rutas de acceso
node_modules
y las moverá a otro archivo 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 del entorno de ejecución de Webpack
Lamentablemente, no basta con extraer solo el código del proveedor. Si intentas cambiar algo en el código de la aplicación:
// index.js
…
…
// E.g. add this:
console.log('Wat');
Notará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 una entorno de ejecución: Es un fragmento pequeño de código. que administra la ejecución del módulo. Cuando divides el código en varios archivos, este fragmento de código empieza a incluir una asignación entre los IDs de bloque y 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 este caso. Y cada vez que un bloque cambia, este fragmento de 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 el Webpack 4, es
Para ello, habilita la opción optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
En webpack 3, haz esto creando un bloque 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úyelos en index.html
en el 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 webpack sobre almacenamiento en caché a largo plazo
- Los documentos de Webpack sobre su entorno de ejecución y manifiesto
- "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 el código HTML. respuesta. Es decir, en lugar de esto:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
haz lo siguiente:
<!-- 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 alinearlo te ayudará a guardar una solicitud HTTP importante con HTTP/1; menos importante con HTTP/2, pero aún podría reproducir un efecto).
o crear a partir de ellos. Te mostramos cómo.
Si generas HTML con el HtmlWebpackPlugin
Si usas HtmlWebpackPlugin para generar un archivo HTML, el InlineSourcePlugin es todo lo que necesitas:
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 con una lógica de servidor personalizada
Con webpack 4:
Agrega el
WebpackManifestPlugin
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ía un archivo similar al siguiente:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
Intercala el contenido del bloque de tiempo de ejecución de una manera 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 bien, con webpack 3:
Haz que el nombre del entorno de ejecución sea estático mediante la especificación de
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Intercala el contenido de
runtime.js
de una 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 partes cada vez más y menos importantes:
- Si cargas una página de video en YouTube, te importa más el video que el comentarios. En este caso, 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 que sobre los anuncios. En este caso, el texto es más importante que los anuncios.
En esos casos, mejora el rendimiento de carga inicial descargando solo el
con lo más importante primero
y la carga diferida de las partes restantes después. Utiliza el
Función import()
y
code-splitting 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 deseas cargar un módulo específico de forma dinámica. Cuándo
webpack detecta import('./module.js')
, y mueve este módulo a otro módulo
chunk:
$ 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 solo lo descarga cuando la ejecución alcanza la función import()
.
De esta manera, el paquete main
se reducirá, lo que mejorará el tiempo de carga inicial.
Además, mejorará el almacenamiento en caché: si cambias el código del bloque principal,
el bloque de comentarios no se verá afectado.
Lecturas adicionales
- Documentación de Webpack para
import()
función - La propuesta de JavaScript para implementar
import()
sintaxis
Divide el código en rutas y páginas
Si tu app tiene varias rutas o páginas, pero hay un solo archivo JS con
el código (un solo bloque de 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:
![Una página principal de WebFundamentals](https://web.developers.google.cn/static/articles/use-long-term-caching/image/a-webfundamentals-home-pa-086040f923b7f.png?authuser=7&hl=es-419)
no necesitan cargar el código para procesar un artículo que se encuentra en una pero la cargarán. Además, si el usuario siempre visita solo la casa y realizas un cambio en el código del artículo, webpack invalidará el todo el paquete, y el usuario tendrá que volver a descargar la app completa.
Si dividimos la app en páginas (o rutas, si es una app de una sola página), el usuario descargará solo el código relevante. Además, el navegador almacenará en caché el código de la app es mejor: si cambias el código de la página principal, webpack invalidará solo los fragmento correspondiente.
Para apps de una sola página
Para dividir las apps de una sola página por rutas, usa import()
. Consulta el artículo "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:
- "Código
Dividiendo"
en los documentos de
react-router
(para React) - "Carga diferida
Rutas" en
Documentos de
vue-router
(para Vue.js)
Para apps tradicionales de varias páginas
Para dividir las apps tradicionales por páginas, usa la entrada de Webpack puntos. Si tu app tiene tres tipos de páginas: la página principal, la página del artículo y la página 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 separado y generará un paquete que incluya solo los módulos utilizados por 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 la incluirá, y el usuario no tendrá que descargarla cuando
visitando 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, ambas entradas
puntos incluirán una copia de Lodash. Para resolver esto, en el paquete web 4,agrega lo siguiente:
optimization.splitChunks.chunks: 'all'
en la configuración de tu webpack:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Esta opción permite la división de código inteligente. Con esta opción, webpack se crea automáticamente buscar código común y extraerlo en archivos separados.
O bien, en el paquete web 3, usa CommonsChunkPlugin
.
moverá las dependencias comunes a un nuevo archivo especificado:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
No dudes en jugar con el valor de minChunks
para encontrar el mejor. En general,
quieres que sea pequeño, pero aumentarlo si aumenta la cantidad de fragmentos. Para
Por ejemplo, para 3 fragmentos, minChunks
puede ser 2, pero para 30 fragmentos, puede ser 8.
porque si lo mantienes en 2, se incluirán demasiados módulos en el archivo común,
pero no lo habías hecho.
Lecturas adicionales
- Documentación de Webpack sobre el concepto de entrada puntos
- Documentación de Webpack acerca de la CommonsChunkPlugin
- "Aprovechar al máximo CommonsChunkPlugin"
- Cómo funcionan
optimization.splitChunks
yoptimization.runtimeChunk
Hacer que los IDs de módulo sean más estables
Cuando compilas el código, webpack asigna un ID a cada módulo. Más adelante, estos IDs se
que se usa en los objetos require()
del paquete. Por lo general, ves los IDs en el resultado de la compilación
justo antes de las rutas 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 usando un contador (es decir, el primer módulo tiene el ID 0, la segunda tiene el ID 1, y así sucesivamente). El problema con esto es que cuando agregas un módulo nuevo, podría aparecer en el medio de la lista de módulos, cambiando todos los próximos módulos ID:
$ 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 una nueva módulo...
[4] ./webPlayer.js 24 kB {1} [built]
↓ ¡Mira lo que hizo! 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 dependen de ellos:
incluso si su código real no cambió. En nuestro caso, el bloque 0
(el fragmento
con comments.js
) y el bloque main
(el bloque con el otro código de la app)
invalidado, mientras que solo el main
debería haber sido.
Para resolver esto, cambia cómo se calculan los IDs de módulo usando la
HashedModuleIdsPlugin
Reemplaza los ID basados en contadores con hashes de rutas de acceso del módulo:
$ 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 módulo. Los módulos nuevos no afectarán a otros módulos de sus IDs.
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 acerca de la HashedModuleIdsPlugin
En resumen
- Almacena en caché el paquete y cambia el nombre del paquete para diferenciar entre las versiones
- 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
- Código no crítico de carga diferida con
import
- Divide el código por rutas o páginas para evitar cargar elementos innecesarios