Aprovechar el almacenamiento en caché a largo plazo

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:

  1. 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.

  2. 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

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:

  1. 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!

  2. 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.

  3. 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 llamado vendor.[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

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:

  1. 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"
    }
    
  2. 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:

  1. 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'
        })
      ]
    };
    
  2. 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

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

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:

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

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

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