Usar o armazenamento em cache de longo prazo

Como o webpack ajuda no armazenamento em cache de recursos

A próxima etapa (depois de otimizar o tamanho do app) que melhora o tempo de carregamento do app é o armazenamento em cache. Use para manter partes do app no e evite sempre fazer o download deles novamente.

Usar o controle de versões de pacotes e os cabeçalhos de cache

A abordagem comum de fazer o armazenamento em cache é:

  1. pedir ao navegador para armazenar um arquivo em cache por um período muito longo (por exemplo, um ano):

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

    Se você não sabe o que o Cache-Control faz, consulte o livro (em inglês) de Jake Archibald excelente postagem sobre práticas recomendadas práticas recomendadas.

  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>
    

Esse método instrui o navegador a baixar o arquivo JS, armazená-lo em cache e usar a cópia em cache. O navegador só acessará a rede se o nome do arquivo for alterado (ou se passar um ano).

Com o webpack, você faz o mesmo, mas em vez de um número de versão, você especifica a 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 da para enviá-lo ao cliente, use o HtmlWebpackPlugin ou o WebpackManifestPlugin.

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

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

O WebpackManifestPlugin é uma abordagem mais flexível, útil se você tiver uma parte complexa do servidor. Durante o build, ele gera um arquivo JSON com um mapeamento entre os nomes dos arquivos sem hash e nomes de arquivo 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 real do app. Ao se mover em um arquivo separado, o navegador poderá armazená-los 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]. com o nome de um bloco. Se não adicionarmos a parte [name], teremos para diferenciar 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 em lugar de [name] da etapa 1.

    Até agora, se você criar o aplicativo, esse bloco incluirá todo o código do aplicativo, apenas como se não tivéssemos concluído essas etapas. Mas isso vai 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 ativa a divisão inteligente de código. Com ele, o webpack extrairia o código do fornecedor se ele chega a mais de 30 KB (antes da minificação e do gzip). Ele também extrairia o código comum, isso é útil se o build produz 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 que incluem node_modules e caminhos move os arquivos 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. E tudo bem:

$ 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 faria o download novamente apenas do código alterado.

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

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

// index.js
…
…

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

você perceberá 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 webpack, além do código dos módulos, tem um ambiente de execução: um pequeno trecho de código que gerencia a execução do módulo. Ao dividir o código em vários arquivos, este código começa a incluir um mapeamento entre IDs de blocos e 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 é 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
    })
  ]
};

Após essas 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

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

Para melhorar ainda mais, tente in-line o tempo de execução do webpack no código HTML resposta. Ou seja, 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 in-line ajuda a economizar uma solicitação HTTP importante com HTTP/1. menos importante com o HTTP/2, mas ainda pode ter ).

É muito fácil:

Se você gerar HTML com o plug-in HTMLWebpackPlugin

Se você usar o método HtmlWebpackPlugin para gerar um arquivo HTML, o InlineSourcePlugin é tudo de 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 método 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 da parte do tempo 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. In-line o conteúdo 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>
        …
      `);
    });
    

Carregar lentamente o código que você não precisa no momento

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

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

Nesses casos, melhore o desempenho do carregamento inicial fazendo o download apenas do as partes mais importantes primeiro e o carregamento lento das partes restantes depois. Use o função import() e code-splitting 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 detectar o import('./module.js'), ele moverá esse módulo para um 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

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. Isso melhora ainda mais o armazenamento em cache. Se você alterar o código na parte principal, 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 (um único bloco de main), é provável que você esteja disponibilizando 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 que está em uma página, mas eles a carregarão. Além disso, se o usuário sempre visita apenas a casa página e fizer uma alteração no código do artigo, o webpack vai invalidar o o pacote inteiro, e o usuário terá que fazer o download de todo o aplicativo novamente.

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

Para apps de página única

Para dividir apps de página única por rotas, use import(). Consulte a página que você não precisa no momento”). Se você usar uma estrutura, ele pode já ter uma solução para isso:

Para aplicativos tradicionais de várias páginas

Para dividir apps tradicionais por páginas, use a entrada do webpack pontos. Se o app tem três tipos de página: a página inicial, a página do artigo e a página da conta de usuário, deve 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 o Lodash, os pacotes home e profile não inclui o arquivo, e o usuário não precisa fazer o download da biblioteca quando acessando a página inicial.

Árvores de dependência separadas têm desvantagens. Se dois pontos de entrada usarem Lodash, e você não moveu suas dependências para um pacote de fornecedor, tanto a entrada incluindo uma cópia do Lodash. Para resolver isso, no webpack 4,adicione o optimization.splitChunks.chunks: 'all' na configuração do webpack:

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

Essa opção ativa a divisão inteligente de código. Com essa opção, o webpack automaticamente procure códigos comuns e os extraia em arquivos separados.

Ou, 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 minChunks para encontrar o melhor. Geralmente, você quer mantê-lo pequeno, mas aumentar se o número de blocos crescer. Para exemplo, para 3 blocos, minChunks pode ser 2, mas para 30 blocos, pode ser 8. porque se você mantiver em 2, muitos módulos entrarão no arquivo comum, inflando demais.

Leitura adicional

Tornar os IDs dos módulos mais estáveis

Ao criar o código, o webpack atribui um ID a cada módulo. Depois, esses IDs são usada 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 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, alterando todos os próximos módulos" IDs:

$ 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]

↓ 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 alterado. No nosso caso, o bloco 0 (o bloco com comments.js) e o bloco main (o bloco com o outro código do app) recebem invalidado, enquanto apenas o main deveria ter sido.

Para resolver isso, mude como os IDs dos módulos são calculados usando a função HashedModuleIdsPlugin Ele substitui IDs baseados em contagem por hashes de caminhos do 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ó muda se você renomear ou mover esse mais tarde neste módulo. Os novos módulos não afetarão outros módulos do Google Ads.

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

  • 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
  • In-line o ambiente de execução para salvar uma solicitação HTTP
  • Carregar código não crítico lentamente com import
  • Divida o código por rotas/páginas para evitar o carregamento de itens desnecessários