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 é o armazenamento em cache. Use-o para manter partes do app no cliente e evite fazer o download delas todas as vezes.

Usar o controle de versões do pacote e cabeçalhos de cache

A abordagem comum para fazer o armazenamento em cache é:

  1. informar ao navegador para 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 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 instrui o navegador a fazer o download do arquivo JS, armazená-lo em cache e usar a cópia em cache. O navegador só vai alcançar a rede se o nome do arquivo mudar (ou se passar um ano).

Com o webpack, você faz o mesmo, mas, em vez de um número de versão, você 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 enviá-lo 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, ela será suficiente para você:

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

O WebpackManifestPlugin é uma abordagem mais flexível que é útil se você tem uma parte complexa do servidor. Durante a criação, ele gera um arquivo JSON com mapeamento entre nomes de arquivo 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"
}

Leia mais

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

Dependências

As dependências do app tendem a mudar com menos frequência do que o código real. Se você movê-las para um arquivo separado, o navegador poderá armazená-las em cache separadamente e não 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] por um nome de um bloco. Se não adicionarmos a parte do [name], vamos precisar diferenciar os fragmentos pelo hash deles, o que é muito 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 uma parte. Esse nome será substituído no lugar por [name] da etapa 1.

    A esta altura, se você criar o app, esse bloco vai incluir todo o código dele, da mesma forma que não realizamos essas etapas. Mas isso mudará em alguns segundos.

  3. 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 permite a divisão do código inteligente. Com ele, o webpack extrairia o código do fornecedor se ele ficasse maior que 30 KB (antes da minificação e do gzip). Ela 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 usa todos os módulos com caminhos que incluem node_modules e os move para um arquivo separado chamado vendor.[chunkhash].js.

Após essas 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 de fornecedores poderá não ser gerado se as dependências forem pequenas, mas isso não é um problema:

$ 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 armazenaria esses arquivos em cache separadamente e transferiria apenas o código alterado.

Código de 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');

Observe 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 de módulos, tem um ambiente de execução, um pequeno trecho de código que gerencia a execução do módulo. Quando você divide o código em vários arquivos, esse trecho começa a incluir um mapeamento entre os IDs de bloco e os arquivos correspondentes:

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

O Webpack inclui esse ambiente de execução no último bloco gerado, que é vendor no nosso caso. E toda vez que qualquer bloco muda, esse trecho 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-as 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>

Leia mais

Ambiente de execução do webpack inline para economizar uma solicitação HTTP extra

Para tornar as coisas ainda melhores, tente integrar o ambiente de execução do webpack na resposta HTML. Por exemplo, em vez de:

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

faça isto:

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

O ambiente de execução é pequeno e a inserção in-line ajuda você 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, precisará do 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()
  ]
};

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 bloco do ambiente 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. Inline o conteúdo do bloco do ambiente 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 inline 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ê carrega a página de um vídeo no YouTube, o vídeo é mais importante para você do que os comentários. Aqui, o vídeo é mais importante que os comentários.
  • Ao abrir uma matéria em um site de notícias, você se preocupa mais com o texto do que com os anúncios. Aqui, o texto é mais importante do que os anúncios.

Nesses casos, melhore o desempenho do carregamento inicial fazendo o download apenas do conteúdo mais importante primeiro e carregando lentamente 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 chega à função import().

Isso diminui o tamanho do pacote main, melhorando o tempo de carregamento inicial. Além disso, ela melhora ainda mais o armazenamento em cache. Se você alterar o código no bloco principal, a parte de comentários não será afetada.

Leia mais

Dividir o código em rotas e páginas

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

Uma página inicial do WebFundamentals

eles não precisam carregar o código para renderizar um artigo que está em uma página diferente, mas eles vão carregá-lo. 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 invalidará todo o pacote, e o usuário precisará fazer o download do app inteiro novamente.

Se dividirmos o app em páginas (ou rotas, se for um app de página única), o usuário vai fazer o download apenas do código relevante. Além disso, o navegador armazenará o código do aplicativo em cache melhor: se você alterar o código da página inicial, o webpack 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, é possível que ele já tenha uma solução para isso:

Para apps tradicionais de várias páginas

Para dividir apps tradicionais por páginas, use os pontos de entrada do webpack. Se o app tem três tipos de páginas: a página inicial, a página do artigo 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 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 Lodash, os pacotes home e profile não o vão incluir. Além disso, o usuário não vai precisar fazer o download dessa biblioteca ao acessar a página inicial.

No entanto, árvores de dependência separadas têm desvantagens. Se dois pontos de entrada usarem o Lodash e você não tiver movido as dependências para um pacote do fornecedor, os dois 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 permite a divisão do código inteligente. Com essa opção, o webpack procuraria automaticamente um código comum e o extrairia em arquivos separados.

Ou, no webpack 3,use 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
    })
  ]
};

Teste o valor de minChunks para encontrar o melhor. Em geral, convém mantê-lo pequeno, mas aumente se o número de partes aumentar. Por exemplo, para três partes, minChunks pode ser 2, mas para 30 partes, pode ser 8. Se você mantiver esse valor em 2, muitos módulos entrarão no arquivo comum, aumentando-o demais.

Leia mais

Tornar os IDs de módulos mais estáveis

Ao criar o código, o webpack atribui um ID a cada módulo. Mais tarde, esses IDs são usados em require()s dentro do pacote. Geralmente, os IDs são mostrados na saída do build 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 ID 0, o segundo tem 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]

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

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

ads.js agora tem 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 main (o bloco com o outro código do app) são invalidados, enquanto apenas o main deveria ter sido.

Para resolver isso, mude a forma como os IDs de módulos são calculados usando HashedModuleIdsPlugin. Ela substitui IDs baseados em contador 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

Nessa abordagem, o ID de um módulo só será alterado se você renomear ou mover esse módulo. 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()
  ]
};

Leia mais

Resumo

  • Armazene o pacote em cache e diferencie as versões alterando o nome do pacote
  • Dividir o pacote em código do app, código do fornecedor e ambiente de execução
  • Integrar o ambiente de execução para salvar uma solicitação HTTP
  • Carregamento lento de códigos não críticos com import
  • Divida o código por rotas/páginas para evitar o carregamento de coisas desnecessárias