À medida que o aplicativo se desenvolve e cresce, o tempo de criação também aumenta - de vários minutos durante a remontagem no modo de desenvolvimento a dezenas de minutos durante a montagem de produção "fria". Isso é completamente inaceitável. Nós, desenvolvedores, não gostamos de alternar contextos enquanto esperamos que o pacote esteja pronto e queremos receber feedback do aplicativo o mais cedo possível - idealmente ao mudar do IDE para o navegador.
Como conseguir isso? O que podemos fazer para otimizar o tempo de compilação?
Este artigo é uma visão geral das ferramentas existentes no ecossistema do webpack para acelerar a montagem, suas experiências e dicas.
A otimização do tamanho do pacote e do desempenho do aplicativo em si não é considerada neste artigo.
O projeto, cujas referências são encontradas no texto e com relação às medições da velocidade de montagem, é um aplicativo relativamente pequeno, escrito na pilha JS + Flow + React + Redux, usando webpack, Babel, PostCSS, Sass, etc., e composto por cerca de 30 mil linhas de código e 1.500 módulos. As versões de dependência são atuais a partir de abril de 2019.
Os estudos foram conduzidos em um computador com Windows 10, Node.js 8, um processador de 4 núcleos, 8 GB de memória e SSD.
Terminologia
- Assembly é o processo de conversão dos arquivos de origem do projeto em um conjunto de ativos relacionados que juntos formam um aplicativo Web.
- dev-mode - montagem com o
mode: 'development'
opcional mode: 'development'
, geralmente usando o webpack-dev-server e o watch-mode. - prod-mode - montagem com o
mode: 'production'
opcional mode: 'production'
, geralmente com um conjunto completo de otimizações de pacote. - Compilação incremental - no modo dev: reconstrua apenas arquivos com alterações.
- Construção "fria" - construa a partir do zero, sem caches, mas com dependências instaladas.
Armazenamento em cache
O armazenamento em cache permite salvar os resultados dos cálculos para reutilização adicional. O primeiro assembly pode ser um pouco mais lento que o normal devido à sobrecarga do cache, mas os subsequentes serão muito mais rápidos devido à reutilização dos resultados da compilação de módulos inalterados.
Por padrão, o webpack no modo de exibição armazena em cache os resultados intermediários de construção na memória para não remontar todo o projeto a cada alteração. Para uma compilação normal (não no modo de exibição), essa configuração não faz sentido. Você também pode tentar ativar a resolução de cache para simplificar a pesquisa do webpack por módulos e verificar se essa configuração tem um efeito perceptível no seu projeto.
Não há cache persistente (salvo em disco ou outro armazenamento) no webpack, embora eles prometam adicioná- lo na versão 5. Enquanto isso, podemos usar as seguintes ferramentas:
- Armazenamento em cache nas configurações do TerserWebpackPlugin
Desabilitado por padrão. Mesmo sozinho, ele tem um efeito positivo perceptível: 60,7 s → 39 s (-36%), combina bem com outras ferramentas de cache.
Ligar e usar é muito simples:
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, cache: true }) ] }
- carregador de cache
O carregador de cache pode ser colocado em qualquer cadeia de carregadores e armazenar em cache os resultados de carregadores anteriores.
Por padrão, ele salva o cache na pasta .cache-loader na raiz do projeto. Usando a opção cacheDirectory
nas configurações do carregador, o caminho pode ser redefinido.
Exemplo de uso:
{ test: /\.js$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve( __dirname, 'node_modules/.cache/cache-loader' ), }, }, 'babel-loader' ] }
Solução segura e confiável. Funciona sem problemas com quase qualquer carregador: para scripts (babel-loader, ts-loader), estilos (scss-, less-, postcss-, css-loader), imagens e fontes (image-webpack-loader, react-svg- carregador de arquivos) etc.
Observe:
- Ao usar o cache-loader em conjunto com o style-loader ou o MiniCssExtractPlugin.loader, ele deve ser colocado depois deles:
['style-loader', 'cache-loader', 'css-loader', ...]
. - Ao contrário das recomendações da documentação para usar esse carregador para armazenar em cache os resultados de cálculos trabalhosos, pode resultar em um pequeno mas mensurável aumento de desempenho para os carregadores “mais leves” - você precisa tentar medir.
Resultados:
- dev: 35.5 s → (ativar o cache-loader) → 36.2 s (+ 2%) → (remontagem) → 7.9 s (-78%)
- prod: 60,6 s → (ativar o cache-loader) → 61,5 s (+ 1,5%) → (remontagem) → 30,6 s (-49%) → (ativar o cache do Terser) → 15, 4 s (-75%)
- HardSourceWebpackPlugin
Uma solução mais maciça e "inteligente" para armazenar em cache no nível de todo o processo de montagem, em vez de cadeias individuais de carregadeiras. No caso de uso básico, basta adicionar o plug-in à configuração do webpack, as configurações padrão devem ser suficientes para a operação correta. Adequado para quem deseja alcançar o máximo desempenho e não tem medo de enfrentar dificuldades.
plugins: [ ..., new HardSourceWebpackPlugin() ]
A documentação contém exemplos de uso com configurações avançadas e dicas para solucionar possíveis problemas. Antes de colocar o plug-in em operação continuamente, vale a pena testar minuciosamente sua operação em várias situações e modos de montagem.
Resultados:
- dev: 35.5 s → (ativar o plug-in) → 36.5 s (+ 3%) → (remontagem) → 3.7 s (-90%)
- prod: 60,6 s → (ativar o plug-in) → 69,5 s (+ 15%) → (remontagem) → 25 s (-59%) → (ativar o cache do Terser) → 10 s (-83%)
Prós:
- Comparado com o cache-loader, ele acelera ainda mais a remontagem;
- Não requer declarações duplicadas em locais diferentes da configuração, como no cache-loader.
Contras:
- Comparado ao cache-loader, diminui ainda mais a primeira compilação (quando não há cache de disco);
- pode aumentar levemente o tempo de reconstrução incremental;
- pode causar problemas ao usar o webpack-dev-server e exigir configuração detalhada da separação e invalidação do cache (consulte a documentação );
- alguns problemas com bugs no GitHub.
- Armazenamento em cache nas configurações do babel-loader . Desabilitado por padrão. O efeito é vários por cento pior que o do cache-loader.
- Armazenamento em cache nas configurações do eslint-loader . Desabilitado por padrão. Se você usar esse carregador, o cache ajudará você a não perder tempo alinhando arquivos inalterados durante a remontagem.
Ao usar o cache-loader ou o HardSourceWebpackPlugin, você precisa desativar os mecanismos de cache internos em outros plug-ins ou carregadores (exceto o TerserWebpackPlugin), pois eles deixarão de ser úteis em construções repetidas e incrementais, e as que são "frias" até ficarão mais lentas. O mesmo se aplica ao próprio carregador de cache se o HardSourceWebpackPlugin já estiver em uso.
Ao configurar o armazenamento em cache, as seguintes perguntas podem surgir:
Onde os resultados do cache devem ser armazenados?
node_modules/.cache/<_>/
são geralmente armazenados no node_modules/.cache/<_>/
. A maioria das ferramentas usa esse caminho por padrão e permite substituí-lo se você deseja armazenar o cache em outro local.
Quando e como invalidar o cache?
É muito importante liberar o cache quando forem feitas alterações na configuração da montagem, o que afetará a saída. O uso do cache antigo nesses casos é prejudicial e pode levar a erros de natureza desconhecida.
Fatores a serem considerados:
- lista de dependências e suas versões: package.json, package-lock.json, yarn.lock, .yarn-integridade;
- conteúdo do webpack, Babel, PostCSS, lista de navegadores e outros arquivos de configuração que são explícita ou implicitamente usados por carregadores e plug-ins.
Se você não usa o cache-loader ou o HardSourceWebpackPlugin, que permite redefinir a lista de fontes para formar a impressão digital do assembly, os scripts npm que limpam o cache ao adicionar, atualizar ou remover dependências ajudarão um pouco mais:
"prunecaches": "rimraf ./node_modules/.cache/", "postinstall": "npm run prunecaches", "postuninstall": "npm run prunecaches"
O Nodemon configurado para limpar o cache e reiniciar o webpack-dev-server ao detectar alterações nos arquivos de configuração também ajudará:
"start": "cross-env NODE_ENV=development nodemon --exec \"webpack-dev-server --config webpack.config.dev.js\""
nodemon.json
{ "watch": [ "webpack.config.dev.js", "babel.config.js", "more configs...", ], "events": { "restart": "yarn prunecaches" } }
Preciso salvar o cache no repositório do projeto?
Como o cache é, de fato, um artefato de montagem, não é necessário confirmá-lo no repositório. A localização do cache dentro da pasta node_modules, que, como regra, está incluída no .gitignore, ajudará com isso.
É importante notar que, se houvesse um sistema de cache que pudesse determinar com segurança a validade do cache sob quaisquer condições, incluindo a alteração do sistema operacional e da versão do Node.js, o cache poderia ser reutilizado entre máquinas de desenvolvimento ou no IC, o que reduziria drasticamente o tempo mesmo da primeira compilação após alternando entre filiais.
Em que modos de compilação vale a pena e em que não vale a pena usar um cache?
Não há uma resposta definitiva aqui: tudo depende da intensidade com que você usa os modos dev e prod durante o desenvolvimento e alterna entre eles. Em geral, nada impede que o cache seja ativado em todos os lugares, mas lembre-se de que geralmente diminui a velocidade da primeira compilação. No IC, você provavelmente sempre precisará de uma compilação "limpa"; nesse caso, o cache pode ser desativado usando a variável de ambiente apropriada.
Materiais interessantes sobre armazenamento em cache no webpack:
Paralelização
Usando a paralelização, você pode obter um aumento no desempenho usando todos os núcleos de processador disponíveis. O efeito final é individual para cada carro.
A propósito, aqui está um código simples do Node.js. para obter o número de núcleos de processador disponíveis (pode ser útil ao configurar as ferramentas listadas abaixo):
const os = require('os'); const cores = os.cpus().length;
- Paralelização nas configurações do TerserWebpackPlugin
Desabilitado por padrão. Além de seu próprio armazenamento em cache, ele é ativado com facilidade e acelera notavelmente a montagem.
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, parallel: true }) ] }
- carregador de linha
O carregador de segmentos pode ser colocado em uma cadeia de carregadores que executam cálculos pesados, após o qual os carregadores anteriores usarão o pool de subprocessos Node.js.
Possui um conjunto de opções que permitem ajustar o trabalho do conjunto de trabalhadores, embora os valores básicos pareçam bastante adequados. poolTimeout
e workers
merecem atenção especial - veja um exemplo .
Ele pode ser usado junto com o cache-loader da seguinte maneira (a ordem é importante): ['cache-loader', 'thread-loader', 'babel-loader']
. Se o aquecimento estiver ativado para o carregador de threads, verifique novamente a estabilidade de montagens repetidas que usam o cache - o webpack pode travar e não concluir o processo após a conclusão da montagem. Nesse caso, basta desligar o aquecimento.
Se você encontrar um travamento de construção após adicionar um carregador de encadeamentos à cadeia de compilação no estilo Sass, essa dica poderá ajudar.
- happypack
Um plug-in que intercepta chamadas de carregadores e distribui seu trabalho por vários segmentos. No momento, ele está no modo de suporte (ou seja, o desenvolvimento não é planejado) e seu criador recomenda o carregador de threads como um substituto. Portanto, se o seu projeto estiver atualizado, é melhor não usar o HappyPack, embora certamente valha a pena tentar e comparar os resultados com o carregador de threads.
O HappyPack possui uma documentação de configuração compreensível, que, aliás, é bastante incomum: propõe-se mover as configurações do carregador para a chamada do construtor de plug-in e substituir as cadeias do carregador por seu próprio carregador happypack. Essa abordagem não padrão pode causar transtornos ao criar um webpack personalizado "de peças".
O HappyPack suporta uma lista limitada de carregadores ; os principais e os mais usados nesta lista estão presentes, mas o desempenho de outros não é garantido devido a uma possível incompatibilidade da API. Mais informações podem ser encontradas nas questões do projeto.
Recusa de cálculos
Qualquer trabalho leva tempo. Para gastar menos tempo, você precisa evitar trabalhos de pouca utilidade, que podem ser adiados para mais tarde ou que não são necessários nessa situação.
- Aplique carregadores ao menor número de módulos possível
As propriedades de teste, exclusão e inclusão especificam as condições para a inclusão do módulo no processo de processamento pelo carregador. O objetivo é evitar a transformação de módulos que não precisam dessa transformação.
Um exemplo popular é a exceção de node_modules da transpilação via Babel:
rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' } ]
Outro exemplo é que arquivos CSS comuns não precisam ser processados por um pré-processador:
rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ]
- Não ative otimizações de tamanho de pacote configurável no modo dev
Em uma poderosa máquina de desenvolvedor com Internet estável, um aplicativo implantado localmente geralmente inicia rapidamente, mesmo que pesa alguns megabytes. Otimizar um pacote durante a montagem pode levar um tempo muito mais precioso do que economizar na carga.
O conselho diz respeito a JS (Terser, Uglify , etc. ), CSS (cssnano, plugin de otimização de css-assets-webpack), SVG e imagens (SVGO, Imagemin, loader de imagem-webpack), HTML (mintml-html, opção em html-webpack-plugin), etc.
- Não inclua polyfills e transformações no modo dev
Se você usar babel-preset-env, postcss-preset-env ou Autoprefixer - adicione uma configuração separada da lista de navegadores para o modo dev, incluindo apenas os navegadores usados durante o desenvolvimento. Provavelmente, essas são as versões mais recentes do Chrome ou Firefox que suportam perfeitamente os padrões modernos sem polyfills e transformações. Isso evitará trabalho desnecessário.
Exemplo .browserslistrc:
[production] your supported browsers go here... [development] last 2 Chrome versions last 2 Firefox versions last 1 Safari version
- Revise o uso de mapas de origem
A geração dos mapas de origem mais precisos e completos leva um tempo considerável (em nosso projeto - cerca de 30% do tempo de criação do produto com a opção devtool: 'source-map'
). Pense se você precisa de mapas de origem no assembly de prod (localmente e no CI). Pode valer a pena gerá-los somente quando necessário - por exemplo, com base em uma variável de ambiente ou tag no commit.
No modo dev, na maioria dos casos, haverá uma opção bastante leve - 'cheap-eval-source-map'
ou 'cheap-module-eval-source-map'
. Consulte a documentação do webpack para mais detalhes.
- Configure a compactação no Terser
De acordo com a documentação do Terser (o mesmo se aplica ao Uglify), ao minificar o código, a maior parte do tempo é consumida pelas opções de mangle
e compress
. Ajustando-os com precisão, é possível obter a aceleração da montagem ao custo de um pequeno aumento no tamanho do pacote. Há um exemplo nas fontes do vue-cli e outro exemplo de um engenheiro do Slack. Em nosso projeto, o ajuste da Terser na primeira modalidade reduz o tempo de montagem em cerca de 7% em troca de um aumento de 2,5% no tamanho do pacote. Se o jogo vale a pena, a decisão é sua.
- Excluir dependências externas da análise
Usando as resolve.alias
e resolve.alias
você pode redirecionar a importação de módulos de biblioteca para versões já compiladas e simplesmente inseri-las no pacote configurável sem perder tempo analisando. No modo dev, isso deve aumentar significativamente a velocidade de montagem, inclusive incremental.
O algoritmo é aproximadamente o seguinte:
(1) Faça uma lista de módulos que precisam ser ignorados ao analisar.
Idealmente, essas são todas as dependências de tempo de execução que se enquadram no pacote (ou pelo menos a mais massiva delas, como reação ou lodash), e não apenas as próprias (primeiro nível), mas também transitivas (dependências de dependência). No futuro, você precisará manter essa lista por conta própria.
(2) Para os módulos selecionados, escreva os caminhos para suas versões compiladas.
Em vez de ignorar dependências, você precisa fornecer ao coletor uma alternativa, e essa alternativa não deve depender do ambiente - faça chamadas para module.exports
, require
, process
, import
, etc. Módulos de arquivo único pré-compilados (não necessariamente minificados), que geralmente estão na pasta dist, dentro das fontes de dependência, são adequados para essa função. Para encontrá-los, você precisa ir para node_modules. Por exemplo, para axios, o caminho para o módulo compilado se parece com: node_modules/axios/dist/axios.js
.
(3) Na configuração do webpack, use a opção resolve.alias para substituir importações por nomes de dependência por importações diretas de arquivos cujos caminhos foram gravados na etapa anterior.
Por exemplo:
{ resolve: { alias: { axios: path.resolve( __dirname, 'node_modules/dist/axios.min.js' ), ... } } }
Há uma grande falha aqui: se o seu código ou o código de suas dependências não acessar o ponto de entrada padrão (arquivo de índice, campo main
em package.json
), mas um arquivo específico dentro das fontes de dependência, ou se a dependência for exportada como um módulo ES, ou se o processo de resolução está interferindo em algo (por exemplo, babel-plugin-transform-imports), toda a idéia pode falhar. O pacote será montado, mas o aplicativo será quebrado.
(4) Na configuração do webpack, use a opção module.noParse para ignorar a análise de módulos pré-compilados solicitados pelos caminhos da etapa 2 usando expressões regulares.
Por exemplo:
{ module: { noParse: [ new RegExp('node_modules/dist/axios.min.js'), ... ] } }
Conclusão: no papel, o método parece promissor, mas uma configuração não trivial com armadilhas aumenta ao menos os custos de implementação e, no mínimo, reduz os benefícios.
Uma alternativa com um princípio de operação semelhante é usar a opção externals
. Nesse caso, você precisará inserir links independentemente para scripts externos no arquivo HTML, e mesmo com as versões de dependência necessárias correspondentes ao package.json.
- Separe raramente o código que muda em um pacote separado e compile-o apenas uma vez
Certamente você ouviu falar sobre o DllPlugin . Com ele, é possível distribuir ativamente o código alterado (seu aplicativo) e raramente o código alterado (por exemplo, dependências) em diferentes assemblies. Depois que o pacote de dependência montado (a mesma DLL) é simplesmente conectado ao assembly do aplicativo, ele economiza tempo.
Parece assim em termos gerais:
- Para criar a DLL, é criada uma configuração separada do webpack, os módulos necessários são conectados como pontos de entrada.
- A compilação começa com esta configuração. DllPlugin gera um pacote DLL e um arquivo de manifesto com nomes de mapas e caminhos de módulos.
- DllReferencePlugin é adicionado à configuração do assembly principal, para o qual o manifesto é passado.
- As importações de dependências renderizadas em DLLs durante a montagem são mapeadas para módulos já compilados usando o manifesto.
Você pode ler um pouco mais no artigo aqui .
Começando a usar essa abordagem, você encontrará rapidamente várias desvantagens:
- O assembly DLL é isolado do assembly principal e precisa ser gerenciado separadamente: prepare uma configuração especial, reinicie-a sempre que uma ramificação for alternada ou uma dependência for alterada.
- Como a DLL não está relacionada aos artefatos do assembly principal, ela deverá ser copiada manualmente para a pasta com os outros ativos e incluída no arquivo HTML usando um destes plug-ins: 1 , 2 .
- É necessário manter manualmente atualizada a lista de dependências destinadas à inclusão no pacote DLL.
- O mais triste: a trepidação de árvores não é aplicada ao pacote DLL. Em teoria, a opção
entryOnly
é destinada a isso, mas eles esqueceram de documentá-la.
Você pode se livrar do clichê e resolver o primeiro problema (e o segundo, se você usar o html-webpack-plugin v3 - ele não funciona com a versão 4) usando o AutoDllPlugin . No entanto, ele ainda não suporta a opção entryOnly
para o entryOnly
usado "por baixo do capô", e o autor do plug-in duvida da conveniência de usar sua ideia à luz do próximo webpack 5.
Diversos
Atualize seu software e dependências regularmente. Node.js, npm / yarn (webpack, Babel .) . , changelog, issues, , .
PostCSS postcss-preset-env stage, . , stage-3, Custom Properties, stage-4 13%.
Sass (node-sass, sass-loader), Dart Sass ( Sass Dart, JS) fast-sass-loader . , . — dart-sass , node-sass, JS, libsass.
Dart Sass sass-loader . Sass fibers.
CSS-, dev-. - , , , .
Um exemplo:
{ loader: 'css-loader', options: { modules: true, localIdentName: isDev ? '[path][name][local]' : '[hash:base64:5]' } }
, , : .
, - webpack PrefetchPlugin , , — . webpack issues , . ?
- . CLI-
--json
, . . , , dev- . - - Hints.
- , “Long module build chains”. , — PrefetchPlugin .
- PrefetchPlugin. . StackOverflow .
: .
, (TypeScript, Angular .) — !
Fontes
, , , .