Usar o armazenamento em cache de longo prazo

Como o webpack ajuda no armazenamento em cache de recursos

A próxima coisa (depois de otimizar o tamanho do app) que melhora o tempo de carregamento do app é o armazenamento em cache. Use-o para manter partes do app no cliente e evite fazer o download delas todas as vezes.

Usar a versão do pacote e os cabeçalhos de cache

A abordagem comum de fazer o armazenamento em cache é:

  1. instrua o navegador a armazenar um arquivo em cache por muito tempo (por exemplo, um ano):

    # Server header
    Cache-Control: max-age=31536000
    

    Se você não sabe o que o Cache-Control faz, consulte a excelente postagem de Jake Archibald sobre práticas recomendadas de armazenamento em cache.

  2. e renomeie o arquivo quando ele for alterado para forçar o novo download:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Essa abordagem informa ao navegador para fazer o download do arquivo JS, armazená-lo em cache e usar a cópia em cache. O navegador só vai acessar a rede se o nome do arquivo mudar ou se um ano passar.

Com o webpack, você faz o mesmo, mas, em vez de um número de versão, especifica o hash do arquivo. Para incluir o hash no nome do arquivo, use [chunkhash]:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Se você precisar do nome do arquivo para enviar ao cliente, use HtmlWebpackPlugin ou WebpackManifestPlugin.

O HtmlWebpackPlugin é uma abordagem simples, mas menos flexível. Durante a compilação, esse plug-in gera um arquivo HTML que inclui todos os recursos compilados. Se a lógica do servidor não for complexa, isso será suficiente para você:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

O WebpackManifestPlugin é uma abordagem mais flexível e útil se você tiver uma parte complexa do servidor. Durante o build, ele gera um arquivo JSON com um mapeamento entre nomes de arquivos sem hash e nomes de arquivos com hash. Use este JSON no servidor para descobrir com qual arquivo trabalhar:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Leitura adicional

Extrair dependências e ambiente de execução em um arquivo separado

Dependências

As dependências de apps tendem a mudar com menos frequência do que o código do app. Se você movê-los para um arquivo separado, o navegador poderá armazená-los em cache separadamente e não os fará o download novamente sempre que o código do app mudar.

Para extrair dependências em um bloco separado, execute três etapas:

  1. Substitua o nome do arquivo de saída por [name].[chunkname].js:

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Quando o webpack cria o app, ele substitui [name] pelo nome de um bloco. Se não adicionarmos a parte [name], teremos que diferenciar entre as partes pelo hash delas, o que é bem difícil.

  2. Converta o campo entry em um objeto:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    Neste snippet, "main" é o nome de um bloco. Esse nome será substituído por [name] na etapa 1.

    Se você criar o app, esse bloco vai incluir todo o código do app, como se essas etapas não tivessem sido realizadas. Mas isso vai mudar em um segundo.

  3. No webpack 4, adicione a opção optimization.splitChunks.chunks: 'all' à sua configuração do webpack:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Essa opção ativa a divisão inteligente do código. Com ele, o Webpack extrairia o código do fornecedor se ele ficasse maior que 30 kB (antes da minificação e do gzip). Ele também extrairia o código comum. Isso é útil se o build produzir vários pacotes (por exemplo, se você dividir seu app em rotas).

    No webpack 3, adicione o 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'),
        })
      ]
    };
    

    Esse plug-in leva todos os módulos cujos caminhos incluem node_modules e os move para um arquivo separado chamado vendor.[chunkhash].js.

Depois dessas mudanças, cada build vai gerar dois arquivos em vez de um: main.[chunkhash].js e vendor.[chunkhash].js (vendors~main.[chunkhash].js para o webpack 4). No caso do webpack 4, o pacote do fornecedor pode não ser gerado se as dependências forem pequenas, e isso é normal:

$ 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

O navegador armazenará esses arquivos em cache separadamente e fará download novamente apenas do código que for alterado.

Código do ambiente de execução do Webpack

Infelizmente, extrair apenas o código do fornecedor não é suficiente. Se você tentar mudar algo no código do app:

// index.js



// E.g. add this:
console.log('Wat');

Você vai notar que o hash vendor também muda:

                           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

Isso acontece porque o pacote do webpack, além do código dos módulos, tem um ambiente de execução, um pequeno código que gerencia a execução do módulo. Quando você divide o código em vários arquivos, essa parte do código começa a incluir um mapeamento entre os IDs de fragmento e os arquivos correspondentes:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

O Webpack inclui esse tempo de execução no último bloco gerado, que é vendor no nosso caso. E toda vez que um bloco muda, esse pedaço de código também muda, fazendo com que todo o bloco vendor mude.

Para resolver isso, vamos mover o ambiente de execução para um arquivo separado. No webpack 4,isso é feito ativando a opção optimization.runtimeChunk:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

No webpack 3, faça isso criando um bloco vazio extra com o 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
    })
  ]
};

Depois dessas mudanças, cada build vai gerar três arquivos:

$ 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

Inclua-os em index.html na ordem inversa. Pronto:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Leitura adicional

Inline webpack runtime to save an extra HTTP request

Para melhorar ainda mais, tente alinhar o tempo de execução do webpack na resposta HTML. Ou seja, em vez de:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

faça o seguinte:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

O tempo de execução é pequeno, e o inline vai ajudar a salvar uma solicitação HTTP (muito importante com HTTP/1; menos importante com HTTP/2, mas ainda pode ter um efeito).

É muito fácil:

Se você gerar HTML com o HtmlWebpackPlugin

Se você usar o HtmlWebpackPlugin para gerar um arquivo HTML, o InlineSourcePlugin é tudo o que você precisa:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Se você gerar HTML usando uma lógica de servidor personalizada

Com o webpack 4:

  1. Adicione o WebpackManifestPlugin para saber o nome gerado do fragmento de tempo de execução:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Um build com esse plug-in criaria um arquivo parecido com este:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Insira o conteúdo do bloco de execução de maneira conveniente. Por exemplo, com Node.js e 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>
    
      `);
    });
    

Ou com o webpack 3:

  1. Torne o nome do ambiente de execução estático especificando filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Insira o conteúdo runtime.js de maneira conveniente. Por exemplo, com Node.js e 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 carregamento lento que você não precisa no momento

Às vezes, uma página tem partes mais e menos importantes:

  • Se você carregar uma página de vídeo no YouTube, vai se importar mais com o vídeo do que com os comentários. Nesse caso, o vídeo é mais importante do que os comentários.
  • Se você abrir um artigo em um site de notícias, vai se importar mais com o texto do artigo do que com os anúncios. Aqui, o texto é mais importante do que os anúncios.

Nesses casos, melhore o desempenho de carregamento inicial fazendo o download apenas dos elementos mais importantes primeiro e carregando as partes restantes mais tarde. Use a função import() e a divisão de código para isso:

// 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 você quer carregar um módulo específico dinamicamente. Quando o webpack encontra import('./module.js'), ele move esse módulo para um bloco separado:

$ 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

e faz o download somente quando a execução atinge a função import().

Isso vai reduzir o pacote main, melhorando o tempo de carregamento inicial. Além disso, o armazenamento em cache vai melhorar. Se você mudar o código no bloco principal, o bloco de comentários não será afetado.

Leitura adicional

Dividir o código em rotas e páginas

Se o app tiver várias rotas ou páginas, mas houver apenas um arquivo JS com o código (um único bloco main), é provável que você esteja veiculando bytes extras em cada solicitação. Por exemplo, quando um usuário visita a página inicial do seu site:

Uma página inicial do WebFundamentals

eles não precisam carregar o código para renderizar um artigo em uma página diferente, mas vão fazer isso. Além disso, se o usuário sempre visitar apenas a página inicial e você fizer uma mudança no código do artigo, o webpack vai invalidar todo o pacote, e o usuário terá que fazer o download do app novamente.

Se dividirmos o app em páginas (ou rotas, se for um app de página única), o usuário fará o download apenas do código relevante. Além disso, o navegador armazenará melhor o código do aplicativo em cache: se você mudar o código da página inicial, o webpack vai invalidar apenas o bloco correspondente.

Para apps de página única

Para dividir apps de página única por rotas, use import() (consulte a seção "Código de carregamento lento que você não precisa no momento"). Se você usa um framework, ele pode ter uma solução para isso:

Para apps tradicionais com várias páginas

Para dividir apps tradicionais por páginas, use os pontos de entrada do Webpack. Se o app tiver três tipos de páginas: a página inicial, a página de artigos e a página da conta do usuário, ele precisa ter três entradas:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Para cada arquivo de entrada, o Webpack vai criar uma árvore de dependências separada e gerar um pacote que inclui apenas os módulos usados por essa 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

Portanto, se apenas a página do artigo usar o Lodash, os pacotes home e profile não vão incluí-lo, e o usuário não vai precisar fazer o download dessa biblioteca ao acessar a página inicial.

No entanto, as árvores de dependência separadas têm desvantagens. Se dois pontos de entrada usarem Lodash e você não tiver movido as dependências para um pacote de fornecedor, ambos os pontos de entrada incluirão uma cópia do Lodash. Para resolver isso, no webpack 4, adicione a opção optimization.splitChunks.chunks: 'all' à configuração do webpack:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

Essa opção ativa a divisão inteligente do código. Com essa opção, o Webpack procurava automaticamente o código comum e o extraía em arquivos separados.

Como alternativa, no webpack 3,use o CommonsChunkPlugin. Ele moverá dependências comuns para um novo arquivo especificado:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Fique à vontade para testar o valor de minChunks para encontrar o melhor. Em geral, é melhor manter o tamanho pequeno, mas aumentar se o número de blocos aumentar. Por exemplo, para 3 partes, minChunks pode ser 2, mas para 30 partes, pode ser 8, porque se você mantiver em 2, muitos módulos vão entrar no arquivo comum, aumentando-o demais.

Leitura adicional

Tornar os IDs de módulos mais estáveis

Ao criar o código, o Webpack atribui um ID a cada módulo. Depois, esses IDs são usados em require()s dentro do pacote. Normalmente, os IDs são mostrados na saída do build logo antes dos caminhos do módulo:

$ 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

↓ Aqui

[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

Por padrão, os IDs são calculados usando um contador (ou seja, o primeiro módulo tem o ID 0, o segundo tem o ID 1 e assim por diante). O problema é que, quando você adiciona um novo módulo, ele pode aparecer no meio da lista de módulos, mudando todos os IDs dos próximos 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]

↓ Adicionamos um novo módulo...

[4] ./webPlayer.js 24 kB {1} [built]

↓ E veja o que ele fez! comments.js agora tem o ID 5 em vez de 4

[5] ./comments.js 58 kB {0} [built]

ads.js agora tem o ID 6 em vez de 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Isso invalida todos os blocos que incluem ou dependem de módulos com IDs alterados, mesmo que o código real não tenha sido modificado. No nosso caso, o bloco 0 (o bloco com comments.js) e o bloco main (o bloco com o outro código do app) foram invalidados, enquanto apenas o main deveria ter sido.

Para resolver isso, mude a forma como os IDs de módulo são calculados usando o HashedModuleIdsPlugin. Ele substitui IDs baseados em contadores por hashes de caminhos de 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

↓ Aqui

[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

Com essa abordagem, o ID de um módulo só muda se você renomear ou mover esse módulo. Os novos módulos não afetam os IDs de outros módulos.

Para ativar o plug-in, adicione-o à seção plugins da configuração:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Leitura adicional

Resumo

  • Armazenar o pacote em cache e diferenciar as versões mudando o nome do pacote
  • Dividir o pacote em código do app, código do fornecedor e ambiente de execução
  • In-line o ambiente de execução para salvar uma solicitação HTTP
  • Carregar lentamente códigos não críticos com import
  • Dividir o código por rotas/páginas para evitar o carregamento de itens desnecessários