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 evitar fazer o download delas toda vez.
Usar a versão do pacote e os cabeçalhos de cache
A abordagem comum para fazer o armazenamento em cache é:
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 as práticas melhores 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 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, útil se você tiver uma parte do servidor complexa.
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
- Jake Archibald sobre as práticas recomendadas de armazenamento em cache
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, siga estas 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]
pelo nome de um bloco. Se não adicionarmos a parte[name]
, teremos que diferenciar os blocos pelo hash, 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 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.
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 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 extrai o código comum. Isso é útil se o build produzir vários pacotes (por exemplo, se você dividir o 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 chamadovendor.[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 armazenaria esses arquivos em cache separadamente e reenviaria apenas o código que muda.
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 é
conseguido 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
- Guia do Webpack sobre armazenamento em cache de longo prazo
- Documentação do Webpack sobre o ambiente de execução e o manifesto do webpack
- "Como aproveitar ao máximo o CommonsChunkPlugin"
- Como
optimization.splitChunks
eoptimization.runtimeChunk
funcionam
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. Por exemplo, 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:
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" }
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:
Para tornar o nome do ambiente de execução estático, especifique
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
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 só o faz quando a execução chega à 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
- Documentação do Webpack para a função
import()
- A proposta de JavaScript para implementar a sintaxe
import()
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:
não precisam carregar o código para renderizar um artigo que está em uma página diferente, mas o carregam. 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 armazena em cache o código do app melhor: se você mudar o código da página inicial, o webpack vai invalidar apenas o fragmento 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:
- "Code
Splitting"
nos documentos do
react-router
(para React) - "Roteiros de carregamento
lento" nas
documentações do
vue-router
(para Vue.js)
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 ser incluídos, e o usuário não 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
o Lodash e você não tiver movido as dependências para um pacote do fornecedor, os dois pontos
de entrada vão incluir 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.
Ou, no webpack 3, use o CommonsChunkPlugin
,
que move 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 três blocos, minChunks
pode ser 2, mas para 30 blocos, pode ser 8,
porque, se você mantiver o valor em 2, muitos módulos vão entrar no arquivo comum,
inflando-o demais.
Leitura adicional
- Documentos do Webpack sobre o conceito de pontos de entrada
- Documentação do Webpack sobre o CommonsChunkPlugin (em inglês)
- "Como aproveitar ao máximo o CommonsChunkPlugin"
- Como
optimization.splitChunks
eoptimization.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. Depois, esses IDs são
usados em require()
s dentro do pacote. Normalmente, os IDs aparecem 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 chunks que incluem ou dependem de módulos com IDs alterados,
mesmo que o código real não tenha mudado. 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
- Documentação do Webpack sobre o HashedModuleIdsPlugin (em inglês)
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
- Inserir o ambiente de execução inline 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