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 é:
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.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
- Jake Archibald sobre práticas recomendadas de armazenamento em cache
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:
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.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.
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 chamadovendor.[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
- Guia do Webpack sobre armazenamento em cache de longo prazo
- Documentos do Webpack sobre o ambiente de execução e o manifesto do webpack
- "Como aproveitar ao máximo o CommonsChunkPlugin"
- Como o
optimization.splitChunks
e ooptimization.runtimeChunk
funcionam
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:
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" }
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:
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' }) ] };
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
- Documentos do Webpack para a função
import()
(link em inglês) - A proposta JavaScript para implementar a sintaxe
import()
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:
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:
- "Divisão de
código" (link em inglês)
nos documentos de
react-router
(para React) - "Rotas de carregamento
lento" nos
documentos de
vue-router
(para Vue.js)
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
- Documentos do Webpack sobre o conceito de pontos de entrada (em inglês)
- Documentos do Webpack sobre o CommonsChunkPlugin (em inglês)
- "Como aproveitar ao máximo o CommonsChunkPlugin"
- Como o
optimization.splitChunks
e ooptimization.runtimeChunk
funcionam
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
- Documentos do Webpack sobre o HashedModuleIdsPlugin
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