Aplicativos JS, sites e outros recursos estão se tornando mais complexos e as ferramentas de construção são a realidade do desenvolvimento da web. Os empacotadores ajudam a empacotar, compilar e organizar bibliotecas. Uma das ferramentas de código aberto poderosas e flexíveis que podem ser perfeitamente personalizadas para criar o aplicativo cliente é o Webpack.
Maxim
Sosnov (
crazymax11 ) - O líder de front-end no N1.RU introduziu o Webpack em vários projetos grandes que anteriormente tinham sua própria criação personalizada e contribuiu com vários projetos. Maxim sabe como criar um pacote de sonhos com o Webpack, fazê-lo rapidamente e configurá-lo para que a configuração permaneça limpa, mantida e modular.
A interpretação é diferente do relatório - é uma versão bastante aprimorada dos links de prof. Ao longo da transcrição, os ovos de Páscoa são espalhados em artigos, plugins, minifiers, opções, transpilers e provas das palavras do orador, links para os quais simplesmente não podem ser colocados em um discurso. Se você coletar tudo, o nível de bônus no Webpack será aberto :-)
Integração do Webpack em um projeto típico
Geralmente, o procedimento de implementação é o seguinte: o desenvolvedor, em algum lugar, lê um artigo sobre o Webpack, decide conectá-lo, começa a construí-lo, de alguma forma funciona, tudo começa e por algum tempo o webpack-config funciona - por seis meses, um ano, dois. Localmente, está tudo bem - o sol, o arco-íris e as borboletas. E então usuários reais vêm:
- Em dispositivos móveis, seu site não carrega.
- Tudo funciona para nós. Localmente, está tudo bem!Por precaução, o desenvolvedor analisa tudo e vê que, para dispositivos móveis, o
pacote pesa 7 MB e leva 30 segundos para carregar . Isso não combina com ninguém e o desenvolvedor começa a procurar como resolver o problema - ele pode conectar um carregador ou encontrar um plug-in mágico que resolverá todos os problemas. Milagrosamente, esse plug-in está localizado. Nosso desenvolvedor acessa o webpack-config, tenta instalar, mas a linha de código interfere:
if (process.env.NODE_ENV === 'production') { config.module.rules[7].options.magic = true; }
A linha é traduzida da seguinte forma: "Se a configuração estiver sendo montada para produção, siga a sétima regra e coloque a opção
magic = true
". O desenvolvedor não sabe o que fazer com isso e como resolvê-lo. Esta é uma situação em que você precisa de um pacote de sonhos.
Como coletar um pacote de sonhos?
Primeiro, vamos definir o que é. Primeiro de tudo, o pacote dos sonhos tem duas características principais:
- Pesa um pouco . Quanto menor o peso - mais rápido o usuário obterá um aplicativo funcional. Você não deseja que seu site seja aberto por 15 segundos.
- O usuário baixa apenas o que precisa ser baixado para exibir a página atual do site, e não mais um byte!
E para reduzir o tamanho do pacote configurável, você deve primeiro avaliar seu tamanho.
Avaliar tamanho do pacote
A solução mais popular é o
plug- in
WebpackBundleAnalyzer . Ele coleta estatísticas de criação de aplicativos e renderiza uma página interativa onde você pode ver a localização e o peso de cada módulo.

Se isso não for suficiente, você pode criar um
gráfico de dependência usando
outro plug-in .

Ou
um gráfico de pizza .

Se isso não for suficiente, e você quiser vender o Webpack para profissionais de marketing, poderá
criar um universo inteiro em que cada ponto seja um módulo, como uma estrela no Universo.

Existem muitas ferramentas que avaliam o tamanho do pacote e o monitoram. Há
uma opção na configuração do Webpack que trava a montagem se o pacote pesar muito, por exemplo. Existe um
plug-in duplicado-pacote-verificador-webpack-plugin que impedirá a
criação de um pacote se você tiver pacotes de 2 npm de versões diferentes, por exemplo, Lodash 4.15 e Lodash 4.14.
Como reduzir o pacote
- O mais óbvio é conectar o UglifyJS para reduzir o JavaScript.
- Use carregadores e plug-ins especiais que compactam e otimizam um recurso específico. Por exemplo, css-nano para css ou SVGO , que otimiza o SVG.
- Compacte todos os arquivos diretamente no Webpack através de plugins gzip / brotli .
- Outras ferramentas
Agora vamos entender como jogar o excesso do pacote.
Jogue fora o excesso
Considere isso em um exemplo popular com
moment.js :
import moment from 'moment'
. Se você pegar um aplicativo vazio, importe moment.js e
ReactDOM para ele e passe-o pelo
WebpackBundleAnalyzer , você verá a seguinte imagem.

Acontece que quando você adiciona um dia, uma hora a uma data ou apenas deseja colocar o link "em 15 minutos" usando moment.js, você conecta
230 Kbytes de código ! Por que isso está acontecendo e como é resolvido?
Carregamento de local no momento
Há uma função no momento.js que define os códigos de idioma:
function setLocale(locale) { const localePath = 'locale/' + locale + '.js'; this._currentLocale = require(localePath); }
Pode ser visto a partir do código que o código do idioma é carregado ao longo do caminho dinâmico, ou seja, calculado em tempo de execução. O Webpack age de maneira inteligente e tenta garantir que seu pacote não trava durante a execução do código: ele encontra todos os locais possíveis no projeto e os agrupa. Portanto, o aplicativo pesa muito.

A solução é muito simples - pegamos um
plug -
in padrão do Webpack e dizemos: "Se você vê que alguém quer fazer o download de muitos locais, porque não pode determinar qual deles, basta usar o russo!"

O Webpack aceita apenas o russo e o WebpackBundleAnalyzer mostra 54 Kb, o que já é 200 Kb mais fácil.
Eliminação de código morto
A próxima otimização que nos interessa é a
eliminação do código morto . Considere o seguinte código.
const cond = true; if (!cond) { return false; } return true; someFunction(42);
A maioria das linhas desse código não é necessária no pacote final - o bloco com a condição não será executado, a função após o retorno também. Tudo que você precisa é
return true
. É exatamente isso que a eliminação do código morto é: a ferramenta de construção detecta o código que não pode ser executado e o corta. Há um recurso interessante que o UglifyJS pode fazer isso.
Agora vamos para a mais avançada eliminação de código morto -
método de agitação de árvore .
Árvore tremendo
Digamos que temos um aplicativo que usa o
Lodash . Duvido muito que alguém esteja usando o Lodash inteiro. Provavelmente, várias funções como
get ,
IsEmpty ,
unionBy ou similares são
exploradas .
Quando fazemos o tremor em árvore, queremos que o Webpack “agite” os módulos desnecessários e jogue-os fora, e só temos os necessários. Esta é a árvore tremendo.
Como o tremido de árvore funciona no Webpack
Digamos que você tenha um código como este:
import { a } from './a.js'; console.log(a);
O código é muito simples: de algum módulo, importe a variável a e faça a saída. Mas existem duas variáveis neste módulo:
a e
b . Não precisamos da variável
be queremos removê-la.
export const a = 3 export const b = 4
Quando o Webpack chega, ele converte o código de importação para isso:
var d = require(0); console.log(d["a"]);
Nossa
import
transformou em
require
, mas o
console.log
não mudou.
A dependência do Webpack é convertida no seguinte código:
var a = 3; module.exports["a«] = a; /* unused harmony export b */ var b = 4;
O Webpack saiu da exportação da variável
a e removeu a exportação da variável
b , mas deixou a própria variável, marcando-a com um comentário especial. No código convertido, a variável
b não
é usada e o UglifyJS pode excluí-la.
A agitação da árvore do Webpack funciona apenas se você tiver algum tipo de minificador de código, como UglifyJS ou babel-minify .
Vamos considerar casos mais interessantes - quando o tremor em árvore não funciona.
Quando Tree Shaking não funciona
Caso nº 1. Você escreve o código:
module.exports.a = 3; module.exports.b = 4;
Execute o código no Webpack e ele permanece o mesmo. Isso ocorre porque o bundler organiza a agitação da árvore somente se você usar os módulos ES6. Se você usar os módulos CommonJS, o Tree shake não funcionará.
Caso No. 2. Você escreve código com módulos ES6 e nomeou exportações.
export const a = 3 export const b = 4
Se o seu código for executado através do Babel e você não definiu a opção de
módulos como false , o Babel levará seus módulos para o CommonJS e o Webpack não poderá executar novamente o agitação da árvore, porque ele funciona apenas com os módulos ES6.
module.exports.a = 3; module.exports.b = 4;
Portanto, precisamos ter certeza de que ninguém em nosso plano de montagem transsipará os módulos ES6.
Caso nº 3. Suponha que tenhamos uma classe tão inútil que não faça nada:
export class ShakeMe {}
. Além disso, ainda não o usamos. Quando o Webpack passa pela importação e exportação, o Babel transforma a classe em uma função e o bundler observa que a função não é usada:
var ShakeMe = function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }();
Parece que tudo deve ficar bem, mas se dermos uma olhada mais de perto, veremos que dentro dessa função existe uma variável global
babelHelpers
, da qual alguma função é chamada. Este é um
efeito colateral : o UglifyJS vê que alguma função global está sendo chamada e não corta o código, porque tem medo de que algo seja interrompido.
Quando você escreve classes e as executa no Babel, elas nunca são cortadas. Como isso é corrigido? Existe um hack padronizado - adicione um comentário
/*#__PURE__*/
antes da função:
var ShakeMe = function () { function ShakeMe() { babelHelpers.classCallCheck(this, ShakeMe); } return ShakeMe; }();
Então o UglifyJS acreditará na palavra que a próxima função é pura. Felizmente, o
Babel 7 está fazendo isso agora e, no Babel 6, nada foi removido até o momento.
Regra: se você tiver um efeito colateral em algum lugar, o UglifyJS não fará nada.
Para resumir:
- A trepidação de árvores não funciona para a maioria das bibliotecas do npm , porque todas elas são do CommonJS e são construídas pelo antigo Babel.
- Provavelmente, o tremor em árvore funcionará adequadamente para as bibliotecas que já estão preparadas para isso , por exemplo, Lodash-es, Date-fns e seu código ou bibliotecas.
- O UglifyJS está envolvido na montagem.
- Módulos ES6 usados.
- Sem efeitos colaterais.
Nós descobrimos como reduzir o peso do pacote e agora vamos ensiná-lo a carregar apenas a funcionalidade necessária.
Carregamos apenas a funcionalidade necessária
Dividimos essa parte em duas. Na primeira parte,
apenas o código exigido pelo usuário é carregado : se o usuário visitar a página principal do seu site, ele não carregará as páginas da conta pessoal. No segundo, as
alterações no código levam ao menor recarregamento possível de recursos .
Carregamos apenas o código necessário
Considere a estrutura de um aplicativo imaginário. Tem:
- Ponto de entrada - APP.
- Três páginas: home, pesquisa e cartão.

O primeiro problema que queremos resolver é
emitir um código comum . Vamos denotar o código vermelho como o código comum para todas as páginas, o círculo verde para a página principal e a página de pesquisa. Os números restantes não são particularmente importantes.

Quando o usuário acessa a pesquisa na página principal, ele recarrega a caixa e o círculo uma segunda vez, embora ele já os possua. Idealmente, gostaríamos de ver algo assim.

É bom que o Webpack 4 já tenha um plug-in embutido que faça isso por nós -
SplitChunksPlugin . O plug-in retira o código do aplicativo ou o código dos módulos do nó, que é usado por vários trechos em um trecho separado, garantindo que o trecho com o código comum seja superior a 30 Kb e para carregar a página, é necessário fazer o download de não mais que 5 trechos. A estratégia é ideal: carregar blocos muito pequenos não é rentável, e carregar muitos blocos é
longo e não é tão eficiente quanto fazer o download de menos blocos, mesmo no http2. Para repetir esse comportamento em 2 ou 3 versões do Webpack, tive que escrever 20 a 30 linhas com recursos não documentados. Agora isso está sendo resolvido em uma linha.
Takeaway de CSS
Seria ótimo se ainda retirássemos o CSS para cada bloco em um arquivo separado. Existe uma solução pronta para isso -
Mini-Css-Extract-Plugin . O plug-in apareceu apenas no Webpack 4 e, antes dele, não havia soluções adequadas para essa tarefa - apenas hacks, dor e ferimentos nas pernas. O plug-in
remove o CSS de partes assíncronas e foi criado
especificamente para esta tarefa , que executa perfeitamente.
Recarga mínima possível de recursos
Vamos descobrir como garantir que, ao liberar, por exemplo, um novo bloco promocional na página principal, o usuário
recarregue a menor parte possível do código .
Se tivéssemos versionamento, tudo ficaria bem. Aqui temos a página principal da versão N e após o lançamento do bloco promocional - versão N + 1. O Webpack fornece um mecanismo semelhante imediatamente, usando hash. Depois que o Webpack coleta todos os ativos, nesse caso, app.js, ele calcula o hash do conteúdo e o adiciona ao nome do arquivo para obter o app. [Hash] .js. Este é o
controle de versão que precisamos.

Vamos verificar como funciona. Ligue os hashes, faça alterações na página principal e veja se o código da página principal realmente mudou.Vamos ver que dois arquivos foram alterados: main e app.js.

Por que isso aconteceu, porque é ilógico? Para entender o porquê, vamos dar uma
olhada no app.js. Consiste em três partes:
- código da aplicação
- tempo de execução do webpack;
- links para pedaços assíncronos.
Quando alteramos o código principal, o conteúdo e o hash são alterados, o que significa que o
link para ele também muda no aplicativo. O aplicativo em si também mudará e precisa ser reiniciado. A solução para esse problema é
dividir o app.js em dois trechos: código do aplicativo e tempo de execução do webpack e links para trechos assíncronos. O Webpack 4 faz tudo por nós com uma opção
runtimeChunk , que pesa muito pouco - menos de 2 KB em gzip. A reinicialização para o usuário é praticamente inútil. RuntimeChunk está ativado com apenas uma opção:
optimization: { runtimeChunk: true }
No Webpack 3 e 2, escreveríamos 5-6 linhas, em vez de uma. Isso não é muito mais, mas ainda é um inconveniente desnecessário.

Tudo está ótimo, aprendemos a criar links e tempo de execução! Vamos escrever um novo módulo no main, liberá-lo e - op! - agora, em geral, tudo reinicia.

Porque Vamos ver como os módulos funcionam no webpack.
Módulos Webpack
Suponha que exista um código no qual você adicione os módulos
a ,
b ,
d e
e :
import a from 'a'; import b from 'b'; import d from 'd'; import e from 'e';
O Webpack converte as importações para exigir:
a ,
b ,
d e
e são substituídos por require (0), require (1), require (2) e requere (3).
var a = require(0); var b = require(1); var d = require(2); var e = require(3);
Imagine uma imagem que acontece com muita frequência: você escreve um novo módulo c
import c from 'c';
e cole em algum lugar no meio:
import a from 'a'; import b from 'b'; import c from 'c'; import d from 'd'; import e from 'e';
Quando o Webpack processa tudo, converte a importação do novo módulo em require (2):
var a = require(0); var b = require(1); var c = require(2); var d = require(3); var e = require(4);
Os módulos
d e
e , que eram 2 e 3, receberão os números 3 e 4 - o novo ID. Uma conclusão simples é a seguinte: usar números de série como id é um pouco tolo, mas o Webpack faz isso.
Não use o número de série como identificação exclusiva
Para corrigir o problema, existe uma solução interna do Webpack -
HashedModuleIdsPlugin :
new webpack.HashedModuleIdsPlugin({ hashFunction: 'md4′, hashDigest:'base64′, hashDigestLength: 4, }),
Este plug-in usa 4 caracteres de
hash md4 em vez de identificação digital do caminho absoluto para o arquivo. Com isso, nossa exigência se tornará:
var a = require('YmRl'); var b = require('N2Fl'); var c = require('OWE4′); var d = require('NWQz'); var e = require('YWVj');
Em vez de números, as letras apareceram. Obviamente, há um problema oculto - é uma
colisão de hashes . Nós deparamos com isso uma vez e podemos aconselhá-lo a usar 8 caracteres em vez de 4. Depois de configurar os hashes corretamente, tudo funcionará da maneira que originalmente queríamos.
Agora sabemos como coletar pacotes de sonhos.
- Minify .
- Use a divisão de código .
- Configure hashes .
Aprendemos a colecionar e agora trabalharemos com velocidade.
Como montar um pacote de sonho rapidamente ?
Em nosso N1.RU, o maior aplicativo consiste em 10.000 módulos e, sem otimizações, leva 28 minutos. Conseguimos acelerar a montagem para dois minutos! Como fizemos isso? Existem três maneiras de acelerar qualquer cálculo, e todas as três são aplicáveis ao Webpack.
Paralelização de montagem
A primeira coisa que fizemos foi
paralelizar a assembléia . Para isso, temos:
- O HappyPackPlugin , que envolve seus carregadores em outros carregadores, e realiza todos os cálculos agrupados em processos separados. Isso permite, por exemplo, paralelizar Babel e node-sass.
- carregador de linha . Executa aproximadamente o mesmo que o HappyPackPlugin, usa não apenas processos, mas o conjunto de encadeamentos. Alternar para um encadeamento separado é uma operação dispendiosa, use-a com cuidado e somente se você desejar agrupar operações pesadas e que consomem muitos recursos, como babel ou node-sass. Para carregar o json, por exemplo, a paralelização não é necessária, porque carrega rapidamente.
- Os plug-ins e carregadores que você usa provavelmente já possuem ferramentas de paralelização integradas - basta olhar. Por exemplo, esta opção está no UglifyJS .
Armazenando em cache os resultados da construção
O armazenamento em cache dos resultados da montagem é a maneira mais eficiente de acelerar a montagem do Webpack.
A primeira solução que temos é o
cache-loader . Este é um carregador que entra em uma cadeia de carregadores e salva o resultado da construção de um arquivo específico para uma cadeia específica de carregadores no sistema de arquivos. Na próxima montagem do pacote configurável, se esse arquivo estiver no sistema de arquivos e já tiver sido processado com essa cadeia, o cache-loader receberá os resultados e não chamará os carregadores que estão por trás deles, por exemplo, Babel-loader ou node-sass.
O gráfico mostra o tempo de montagem. Barra azul - 100% de tempo de compilação, sem cache-loader e com ela - 7% mais lento. Isso ocorre porque o carregador de cache gasta um tempo extra salvando caches no sistema de arquivos. Já na segunda montagem, recebemos um lucro tangível - a montagem foi duas vezes mais rápida.

A segunda solução é mais
sofisticada -
HardSourcePlugin . A principal diferença: o cache-loader é apenas um carregador que pode operar apenas em uma cadeia de carregadores com código ou arquivos, e o HardSourcePlugin tem acesso quase total ao ecossistema Webpack, pode operar com outros plugins e carregadores e amplia o ecossistema para armazenar um pouco em cache. O gráfico acima mostra que, no primeiro lançamento, o tempo de criação aumentou 37%, mas no segundo lançamento com todos os caches, aceleramos 5 vezes.

A melhor parte é que você pode usar as duas soluções juntas, o que fazemos no N1.RU. Tenha cuidado, porque há problemas com caches, que discutirei um pouco mais adiante.
Os plugins / carregadores que você já usa podem ter
mecanismos de cache internos . Por exemplo, o
babel-loader possui um sistema de cache muito eficiente, mas por algum motivo está desativado por padrão. A mesma funcionalidade está no
carregador de scripts do tipo impressionante . O plugin
UglifyJS também possui armazenamento em cache, o que funciona muito bem. Ele nos acelerou por vários minutos.
E agora os problemas.
Problemas de armazenamento em cache
- O cache pode não ser validado corretamente .
- As soluções aplicadas podem não funcionar com plug-ins, carregadores, seu código ou entre si . Nesse sentido, o cache-loader é uma solução simples e sem complicações. Mas com o HardSourcePlugin, você precisa ter mais cuidado.
- É difícil estrear se tudo estiver quebrado . Quando o cache não funciona corretamente e ocorre um erro incompreensível, será muito difícil descobrir qual é o problema.
Como economizar na produção?
A última maneira de acelerar um processo é não fazer parte do processo. Vamos pensar em como você pode economizar na produção? O que não podemos fazer? A resposta é curta -
não podemos fazer nada ! Não temos o direito de recusar algo na produção, mas podemos economizar muito no desenvolvimento.
Em que economizar:
- Não colete o mapa de origem até precisarmos deles.
- Use o style-loader em vez de um esquema interessante com remoção e processamento de css através de css loaders. O carregador de estilos em si é muito rápido, porque pega a linha css e a empurra para uma função que insere essa linha na marca de estilo.
- Você pode deixar na lista de navegadores apenas o navegador usado especificamente - provavelmente este é o último chrome . Isso irá acelerar bastante .
- Abandone completamente qualquer otimização de recursos : de UglifyJS, css-nano, gzip / brotli.
A aceleração de compilação é paralelização, armazenamento em cache e recusa de cálculos. Seguindo estas três etapas simples, você pode acelerar muito.
Como configurar o webpack?
Nós descobrimos como montar um pacote de sonhos e como montá-lo rapidamente, e agora vamos descobrir como configurar o Webpack para não nos dar um tiro na perna toda vez que você alterar a configuração.
Configuração da evolução no projeto
Um caminho típico do webpack-config em um projeto começa com uma configuração
simples . Inicialmente, basta inserir o Webpack, o Babel-loader, o sass-loader e tudo está bem. Inesperadamente, algumas
condições aparecem
em process.env e você as insere. Um, segundo, terceiro, mais e mais, até que uma condição com uma opção "mágica" seja adicionada. Você entende que tudo já está muito ruim, e é melhor
duplicar as configurações para dev e produção e fazer as correções duas vezes. Tudo ficará mais claro. Se você pensou: “Algo está errado aqui?”, O único conselho de trabalho é
manter a configuração em ordem . Vou te dizer como fazemos.
Mantenha a configuração em ordem
Usamos o pacote
webpack-merge . Este é um pacote npm criado para combinar várias configurações em uma. Se você não estiver familiarizado com a estratégia de mesclagem padrão, poderá personalizá-la.
4 :
- Loaders.
- Plugins.
- Presets.
- Parts.
.
Plugin/Loader
, , API, , .
Parece algo como isto:
module.exports = function createPlugin(options) { return new Plugin(options); };
, , , . , url-loader :
, , , , , , . , , , , url-loader. :
function urlLoader(prefix = 'assets', limit = 100) { return { loader: 'url-loader', options: { limit, name: `${prefix}/[name].[hash].[ext]` } }; };
. , Loader .
Preset
webpack. , , , webpack, . — , , scss-:
{ test: /\.scss$/, use: [cssLoader, postCssLoader, scssLoader] }
.
Part
— , . , , . , :
entry: { app: './src/Frontend/app.js' }, output: { publicPath: '/static/cabinet/app/', path: path.resolve('www/static/app') },
:
- , , , json, , , splitChunks.
- dev , , js/css
- Part , output, publicPath, entry-point , , source map.
Webpack-merge . , . webpack-merge 3-7 , Babel-loader, . , .
.
, .
, webpack — .
, .
, !
— Frontend Conf . , — , , Frontend Conf ++ .
- ? FrontenConf ++ , 27 28 . 27 , 15 . — !