Encontrar o caminho certo para separar o conteúdo do site usando o Webpack

Encontrar a melhor maneira de organizar os materiais do projeto da web pode ser uma tarefa assustadora. Existem muitos cenários diferentes para os usuários trabalharem com projetos, muitas tecnologias e outros fatores que precisam ser levados em consideração.

O autor do material, cuja tradução publicamos hoje, diz que deseja contar aqui tudo o que você precisa saber para a preparação competente dos materiais do projeto da web para o trabalho. Em primeiro lugar, será sobre como escolher uma estratégia para separar os arquivos do site mais adequados para um projeto específico e para seus usuários. Em segundo lugar, serão considerados os meios para implementar a estratégia escolhida.

imagem

Informação geral


De acordo com o Webpack Glossary , existem duas estratégias de compartilhamento de arquivos. Isso é divisão de pacote e divisão de código. Esses termos podem parecer usados ​​de forma intercambiável, mas não são.

  • A divisão de um pacote configurável é uma técnica para dividir pacotes grandes em várias partes, que são arquivos menores. Esses arquivos, em qualquer caso, como quando se trabalha com um único pacote, serão baixados por todos os usuários do site. A força dessa técnica é melhorar o uso de mecanismos de cache baseados em navegador.
  • A separação de código é uma abordagem que envolve o carregamento dinâmico do código conforme a necessidade. Isso leva ao fato de que o usuário baixa apenas o código necessário para trabalhar com uma determinada parte do site em um determinado momento.

A divisão de código parece ser muito mais interessante do que a divisão de código. E, de fato, há uma sensação de que, em muitos artigos sobre o nosso tópico, o foco principal está na separação de código, essa técnica é considerada a única maneira que vale a pena otimizar os materiais do site.

No entanto, gostaria de dizer que, para muitos sites, é a primeira estratégia muito mais valiosa - a separação de pacotes. E, talvez, literalmente, todos os projetos da Web possam ganhar com sua implementação.

Vamos falar sobre isso com mais detalhes.

Separação de feixes


A técnica para dividir bundles é baseada em uma idéia muito simples. Se você tiver um arquivo enorme e alterar uma única linha de código, o usuário comum precisará fazer o download do arquivo inteiro na próxima vez que visitar o site. No entanto, se você dividir esse arquivo em dois, o mesmo usuário precisará baixar apenas o que foi alterado e o segundo arquivo será retirado do cache do navegador.

Vale ressaltar que, como a otimização dos materiais do site pela divisão de pacotes está vinculada ao cache, os usuários que visitam o site pela primeira vez terão que fazer o download de todos os materiais de qualquer maneira, portanto, não faz diferença para eles se esses materiais serão apresentados como um arquivo ou como vários .

Parece-me que muita conversa sobre o desempenho de projetos da web é dedicada aos usuários que visitam o site pela primeira vez. Talvez isso se deva em parte à importância da primeira impressão que o projeto causará ao usuário, bem como ao fato de que a quantidade de dados transmitidos aos usuários quando eles visitam o site pela primeira vez é simples e conveniente de medir.

Quando se trata de visitantes regulares, pode ser difícil medir o impacto das técnicas de otimização de materiais aplicadas a eles. Mas precisamos simplesmente conhecer as consequências de tais otimizações.

Para analisar essas coisas, você precisa de algo como uma planilha. Você também precisará criar uma lista estrita de condições sob as quais podemos testar cada uma das estratégias de cache estudadas.

Aqui está um script que se encaixa na descrição geral fornecida no parágrafo anterior:

  • Alice visita nosso site uma vez por semana, durante 10 semanas.
  • Atualizamos o site uma vez por semana.
  • Toda semana, atualizamos a página da lista de produtos.
  • Além disso, temos uma página com detalhes do produto, mas ainda não estamos trabalhando nela.
  • Na quinta semana, adicionamos um novo pacote npm aos materiais do projeto.
  • Na oitava semana, atualizamos um dos pacotes npm já usados ​​no projeto.

Existem pessoas (como eu) que tentarão tornar esse cenário o mais realista possível. Mas você não precisa fazer isso. O cenário real aqui realmente não importa. Por que isso é assim - descobriremos em breve.

▍ Condições iniciais


Suponha que o tamanho total do nosso pacote JavaScript seja de considerável 400 Kb e, nas condições atuais, transfira tudo isso para o usuário como um único arquivo main.js Temos uma configuração Webpack, que, em termos gerais, é semelhante à seguinte (removi as coisas que não são relevantes para a nossa conversa):

 const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: {   path: path.resolve(__dirname, 'dist'),   filename: '[name].[contenthash].js', }, }; 

O Webpack nomeia o arquivo main.js resultante quando há uma única entrada na configuração.

Se você não tem uma idéia muito boa de trabalhar com o cache, lembre-se de que toda vez que escrevo main.js aqui, na verdade quero dizer algo como main.xMePWxHo.js . Uma sequência maluca de caracteres é um hash do conteúdo de um arquivo, chamado de contenthash na configuração. O uso dessa abordagem leva ao fato de que, ao alterar o código, os nomes dos arquivos também são alterados, o que força o navegador a baixar novos arquivos.

De acordo com o cenário acima, quando fazemos algumas alterações no código do site toda semana, a linha de contenthash do pacote muda. Como resultado, visitando semanalmente nosso site, Alice é forçada a enviar um novo arquivo de 400 Kb.

Se fizermos um bom tablet (com uma linha de resultados inútil até agora) contendo dados sobre o volume semanal de carregamento de dados por esse arquivo, obteremos o seguinte.


A quantidade de dados carregados pelo usuário

Como resultado, acontece que o usuário, em 10 semanas, baixou 4,12 MB de código. Este indicador pode ser melhorado.

Separação de pacotes de terceiros do código principal


Divida a embalagem grande em duas partes. Nosso próprio código estará no arquivo main.js e o código de terceiros no arquivo vendor.js . É fácil fazer isso, a seguinte configuração do Webpack nos ajudará com isso:

 const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: {   path: path.resolve(__dirname, 'dist'),   filename: '[name].[contenthash].js', }, optimization: {   splitChunks: {     chunks: 'all',   }, }, }; 

O Webpack 4 tenta tornar a vida o mais fácil possível para o desenvolvedor, para que ele faça tudo o que puder e não exija que ele seja informado exatamente como quebrar os pacotes em partes.

Esse tipo de comportamento automático do programa leva a algumas delícias, como: “Bem, que charme esse Webpack é” e a muitas perguntas no espírito: “O que isso é feito com meus pacotes?”.

De qualquer forma, a adição de optimization.splitChunks.chunks = 'all' à configuração da configuração informa ao Webpack que precisamos pegar tudo, desde node_modules e colocá-lo no vendors~main.js

Depois de fazermos uma separação tão básica do pacote, Alice, que regularmente visita nosso site semanalmente, baixa o arquivo main.js de 200 Kb cada vez que ela visita. Mas ela fará o download do arquivo vendor.js apenas três vezes. Isso acontecerá durante as visitas na primeira, quinta e oitava semanas. Aqui está a tabela correspondente, na qual, por vontade do destino, os tamanhos dos vendor.js e vendor.js nas primeiras quatro semanas coincidem e são iguais a 200 Kb.


A quantidade de dados carregados pelo usuário

Como resultado, a quantidade de dados baixados pelo usuário em 10 semanas foi de 2,64 MB. Ou seja, em comparação com o que era antes da separação do pacote, o volume diminuiu 36%. Não é um resultado tão ruim alcançado ao adicionar algumas linhas ao arquivo de configuração. A propósito, antes de ler mais - faça o mesmo em seu projeto. E se você precisar atualizar do Webpack 3 para 4 - faça-o e não se preocupe, pois o processo é bastante simples e ainda é gratuito.

Parece-me que a melhoria considerada aqui parece um pouco abstrata, pois se estende por 10 semanas. No entanto, se considerarmos a quantidade de dados enviados a um usuário fiel, essa é uma redução honesta desse volume em 36%. Este é um resultado muito bom, mas pode ser melhorado.

Packages Destaque pacotes em arquivos separados


O arquivo vendor.js sofre do mesmo problema que o main.js original Consiste no fato de que a alteração de qualquer pacote incluído neste arquivo leva à necessidade de um usuário comum fazer o download do arquivo inteiro novamente.

Por que não criamos arquivos separados para cada pacote npm? Não é difícil fazer isso, então vamos decompor nossa react , lodash , redux , moment e assim por diante em arquivos separados. A seguinte configuração do Webpack nos ajudará com isso:

 const path = require('path'); const webpack = require('webpack'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), plugins: [   new webpack.HashedModuleIdsPlugin(), //        ], output: {   path: path.resolve(__dirname, 'dist'),   filename: '[name].[contenthash].js', }, optimization: {   runtimeChunk: 'single',   splitChunks: {     chunks: 'all',     maxInitialRequests: Infinity,     minSize: 0,     cacheGroups: {       vendor: {         test: /[\\/]node_modules[\\/]/,         name(module) {           //  ,   node_modules/packageName/not/this/part.js           //  node_modules/packageName           const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];           //  npm- ,   ,           //  URL,        @           return `npm.${packageName.replace('@', '')}`;         },       },     },   }, }, }; 

Na documentação, você pode encontrar uma excelente explicação das construções usadas aqui, mas ainda dedico algum tempo para falar sobre algumas coisas, pois levei muito tempo para usá-las corretamente.

  • O Webpack possui instalações padrão bastante razoáveis, que, de fato, não são tão razoáveis. Por exemplo, o número máximo de arquivos de saída é definido como 3, o tamanho mínimo é 30 KB (ou seja, arquivos menores serão mesclados). Eu o redefini.
  • cacheGroups é onde definimos as regras de como o Webpack deve agrupar dados nos arquivos de saída. Eu tenho um grupo aqui, vendor , que será usado para qualquer módulo carregado a partir de node_modules . Normalmente, o nome do arquivo de saída é fornecido como uma string. Mas dei o name como uma função que será chamada para cada arquivo processado. Então eu pego o nome do pacote no caminho do módulo. Como resultado, obtemos um arquivo para cada pacote. Por exemplo, npm.react-dom.899sadfhj4.js .
  • Os nomes dos pacotes, para que possam ser publicados no npm, devem ser adequados para uso em URLs ; portanto, não precisamos executar a operação encodeURI nos nomes dos packageName . No entanto, encontrei um problema em que o servidor .NET se recusa a trabalhar com arquivos cujos nomes contêm o símbolo @ (esses nomes são usados ​​para pacotes com um determinado escopo de nome, os chamados pacotes com escopo definido), então eu, no correspondente fragmento de código, me livre de tais caracteres.

A configuração acima do Webpack é boa, pois você pode configurá-lo uma vez e depois esquecê-lo. Não requer referência a pacotes específicos por nome; portanto, após sua criação, mesmo quando altera a composição dos pacotes, permanece relevante.

Alice, nossa visitante regular, ainda main.js 200 kilobytes toda semana e, na primeira vez em que visita o site, é forçada a baixar 200 KB de pacotes npm, mas não precisa baixar os mesmos pacotes duas vezes.

Abaixo está uma nova versão da tabela com informações sobre o volume de downloads semanais de dados. Por uma estranha coincidência, o tamanho de cada arquivo com pacotes npm é de 20 Kb.


A quantidade de dados carregados pelo usuário

Agora, o volume de dados baixados em 10 semanas é de 2,24 Mb. Isso significa que melhoramos a taxa básica em 44%. O resultado já é muito decente, mas surge a questão de saber se é possível fazê-lo para atingir um resultado superior a 50%. Se isso acontecer, será ótimo.

▍ Divisão do código do aplicativo em fragmentos


Voltamos ao arquivo main.js , que a infeliz Alice precisa baixar constantemente.

Como eu disse acima, existem duas seções separadas em nosso site. O primeiro é uma lista de produtos, o segundo é uma página com informações detalhadas sobre o produto. O tamanho do código, exclusivo para cada um deles, é de 25 Kb (e 150 Kb de código são usados ​​lá e ali).

A página de informações do produto não está sujeita a alterações, como já a aperfeiçoamos. Portanto, se extrairmos seu código em um arquivo separado, esse arquivo, na maioria das vezes trabalhando com o site, será baixado para o navegador do cache.

Além disso, como se viu, temos um enorme arquivo SVG interno usado para renderização de ícone, que pesa até 25 KB e raramente muda.

Algo precisa ser feito com isso.

Criamos manualmente vários pontos de entrada, informando ao Webpack que era necessário criar um arquivo separado para cada uma dessas entidades.

 module.exports = { entry: {   main: path.resolve(__dirname, 'src/index.js'),   ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),   ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),   Icon: path.resolve(__dirname, 'src/Icon/Icon.js'), }, output: {   path: path.resolve(__dirname, 'dist'),   filename: '[name].[contenthash:8].js', }, plugins: [   new webpack.HashedModuleIdsPlugin(), //        ], optimization: {   runtimeChunk: 'single',   splitChunks: {     chunks: 'all',     maxInitialRequests: Infinity,     minSize: 0,     cacheGroups: {       vendor: {         test: /[\\/]node_modules[\\/]/,         name(module) {           //  ,   node_modules/packageName/not/this/part.js           //  node_modules/packageName           const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];           //  npm- ,   ,           //  URL,        @           return `npm.${packageName.replace('@', '')}`;         },       },     },   }, }, }; 

O Webpack trabalhador, além disso, criará arquivos para o que é comum, por exemplo, ProductList e ProductPage , ou seja, não haverá código duplicado.

O que acabamos de fazer permitirá que Alice economize 50 Kb de tráfego quase toda semana. Observe que editamos o arquivo de descrição do ícone na sexta semana. Aqui está a nossa mesa tradicional.


A quantidade de dados carregados pelo usuário

Agora, em apenas dez semanas, apenas 1,815 MB de dados foram baixados. Isso significa que a economia de tráfego foi impressionante em 56%. De acordo com nosso cenário teórico, um usuário comum sempre trabalha com esse nível de economia.

Tudo isso é feito devido a alterações feitas na configuração do Webpack. Não alteramos o código do aplicativo para alcançar esses resultados.

Anteriormente, falei sobre o fato de que o cenário específico em que esse teste é realizado, na verdade, não desempenha um papel especial. Isso se deve ao fato de que, independentemente do cenário utilizado, a conclusão de tudo o que falamos será a mesma: dividir o aplicativo em pequenos arquivos que fazem sentido na aplicação de sua arquitetura nos permite reduzir o volume de dados do site, carregado por seus usuários regulares.

Em breve começaremos a falar sobre separação de código, mas primeiro gostaria de responder três perguntas nas quais você provavelmente está pensando agora.

▍ Pergunta número 1. A necessidade de realizar muitas solicitações não prejudica a velocidade de carregamento do site?


Você pode dar uma resposta curta e simples a esta pergunta: "Não, não faz mal". Uma situação semelhante resultou em um problema nos velhos tempos, quando o protocolo HTTP / 1.1 estava em uso e ao usar o HTTP / 2, isso não é mais relevante.

Embora, note-se que, neste artigo, publicado em 2016 e neste artigo da Khan Academy de 2015, são tiradas as conclusões de que, mesmo ao usar HTTP / 2, o uso de muitos arquivos diminui a velocidade do download. Mas em ambos os materiais, "demais" significa "várias centenas". Portanto, vale lembrar que, se você precisar trabalhar com centenas de arquivos, restrições no processamento de dados paralelos podem afetar a velocidade de download.

Se você estiver interessado, o suporte a HTTP / 2 está disponível no IE 11 no Windows 10. Além disso, fiz um estudo abrangente entre os usuários de sistemas mais antigos. Eles declararam por unanimidade que a velocidade de carregamento do site não era particularmente preocupante.

▍ Pergunta número 2. Os pacotes Webpack têm código auxiliar. Isso cria carga adicional no sistema?


É sim.

▍ Pergunta número 3. Ao trabalhar com muitos arquivos pequenos, seu nível de compactação se deteriora, certo?


Sim, isso também é verdade. Na verdade, eu gostaria de dizer o seguinte:

  • Mais arquivos significa mais código auxiliar do Webpack.
  • Mais arquivos significa menos compactação.

Vamos descobrir para entender como isso é ruim.

Acabei de realizar um teste no qual o código de um arquivo de 190 Kb foi dividido em 19 partes. Isso adicionou cerca de 2% à quantidade de dados enviados ao navegador.

Como resultado, na primeira visita ao site, o usuário fará o upload de 2% a mais de dados e nas visitas subseqüentes - 60% a menos, e isso continuará por muito, muito tempo.
Então vale a pena se preocupar? Não, não vale a pena.

Quando comparei um sistema usando 1 arquivo e um sistema com 19 arquivos, testei-o usando vários protocolos, incluindo HTTP / 1.1. A tabela abaixo apoia fortemente a ideia de que ter mais arquivos significa melhor.


Dados sobre o trabalho com 2 versões de um site hospedado em uma hospedagem estática do Firebase, cujo código tem 190 Kb de tamanho, mas, no primeiro caso, é compactado em 1 arquivo e no segundo é dividido em 19

Ao trabalhar em redes 3G e 4G, o download de um site com 19 arquivos levou 30% menos tempo do que o download de um site com um arquivo.

Há muito ruído nos dados apresentados na tabela. Por exemplo, uma sessão de download de um site por 4G (Execução 2 na tabela) levou 646 ms, outra (Execução 4) - 1116 ms, que é 73% mais longa. Portanto, parece que dizer que o HTTP / 2 é "30% mais rápido" é um pouco desonesto.

Criei esta tabela para ver o que o uso do HTTP / 2 oferece. Mas, de fato, a única coisa que pode ser dita aqui é que o uso do HTTP / 2 provavelmente não afeta particularmente o carregamento da página.

As duas últimas linhas nesta tabela foram uma verdadeira surpresa. Aqui estão os resultados para não a versão mais recente do Windows com IE11 e HTTP / 1.1. Se eu tentasse prever os resultados do teste com antecedência, com certeza diria que essa configuração carregaria os materiais muito mais lentamente do que outros. É verdade que uma conexão de rede muito rápida foi usada aqui, e eu, para esses testes, provavelmente deveria usar algo mais lento.

E agora vou lhe contar uma história. Para explorar meu site em um sistema muito antigo, baixei a máquina virtual do Windows 7 no site da Microsoft. O IE8 foi instalado lá, e decidi atualizar para o IE9. Para fazer isso, fui à página da Microsoft projetada para baixar o IE 9. Mas não pude fazer isso.


Que azar ...

A propósito, se falamos sobre HTTP / 2, quero observar que esse protocolo está integrado ao Node.js. Se você quiser experimentar, pode usar o pequeno servidor HTTP / 2 que escrevi com suporte para o cache de resposta, gzip e brotli.

Talvez eu tenha dito tudo o que queria sobre o método de separação de pacotes. Eu acho que o único ponto negativo dessa abordagem, ao usar quais usuários precisam fazer upload de muitos arquivos, na verdade, não é um "ponto negativo".

Agora vamos falar sobre separação de código.

Separação de código


A idéia principal da técnica de divisão de código é: "Não faça o download de código desnecessário". Disseram-me que o uso dessa abordagem faz sentido apenas para alguns sites.

Prefiro, quando se trata de separação de código, usar a regra 20/20 que acabei de formular. Se alguma parte do site for visitada por apenas 20% dos usuários e sua funcionalidade for fornecida por mais de 20% do código JavaScript do site, esse código precisará ser baixado somente mediante solicitação.

Estes, é claro, não são números absolutos, podem ser ajustados a uma situação específica e, na realidade, existem cenários muito mais complexos do que o descrito acima. A coisa mais importante aqui é o equilíbrio, e é completamente normal não usar a separação de código, se isso não faz sentido para o seu site.

▍ Separado ou não?


Como encontrar a resposta para a pergunta se você precisa de separação de código ou não? Suponha que você tenha uma loja on-line e esteja pensando em separar do restante do código o código usado para receber o pagamento dos clientes, já que apenas 30% dos visitantes compram algo de você.

O que posso dizer? Primeiro, você deve trabalhar para encher a loja e vender algo que seria interessante para mais visitantes do site. Em segundo lugar, você precisa entender quanto código é completamente exclusivo para a seção do site em que o pagamento é aceito. Como você sempre deve fazer uma "divisão de pacotes" antes da "divisão de códigos", e esperamos que faça isso, provavelmente já sabe em que tamanho o código em que estamos interessados ​​tem.

Talvez esse código possa ser menor do que você pensa, portanto, antes de se alegrar com a nova oportunidade de otimizar seu site, você deve calcular tudo com segurança. Se você, por exemplo, possui um site React, as ações do repositório, redutores, sistema de roteamento serão compartilhadas por todas as partes do site. Exclusivo para diferentes partes do código do site será representado principalmente por componentes e funções auxiliares para eles.

Então, você descobriu que um código completamente exclusivo da seção do site usado para pagar as compras leva 7 Kb. O tamanho do restante do código do site é 300 Kb. Em tal situação, eu não participaria da separação de código por vários motivos:

  • Se você baixar esses 7 Kb com antecedência, o site não ficará lento. Lembre-se de que os arquivos são baixados em paralelo e tente medir a diferença necessária para baixar 300 Kb e 307 Kb de código.
  • Se você baixar esse código posteriormente, o usuário terá que esperar depois de clicar no botão "Pagar". E este é o exato momento em que você precisa que tudo corra o mais suavemente possível.
  • A separação de código requer alterações no aplicativo. No código, em locais onde tudo foi feito de forma síncrona antes, a lógica assíncrona aparece. , , , , , .

, , , .

.


, , , .

. , . :

 require('whatwg-fetch'); require('intl'); require('url-polyfill'); require('core-js/web/dom-collections'); require('core-js/es6/map'); require('core-js/es6/string'); require('core-js/es6/array'); require('core-js/es6/object'); 

index.js , :

 import './polyfills'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App/App'; import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root')); } render(); // ,      

Webpack , , npm-. 25 , 90% , .

Webpack 4 import() ( import ), :

 import React from 'react'; import ReactDOM from 'react-dom'; import App from './App/App'; import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root')); } if ( 'fetch' in window && 'Intl' in window && 'URL' in window && 'Map' in window && 'forEach' in NodeList.prototype && 'startsWith' in String.prototype && 'endsWith' in String.prototype && 'includes' in String.prototype && 'includes' in Array.prototype && 'assign' in Object && 'entries' in Object && 'keys' in Object ) { render(); } else { import('./polyfills').then(render); } 

, , , — . — render() . , Webpack npm-, , render() .

, import() Babel dynamic-import . , Webpack, import() , .

, . .

▍ React,


. , , , .

, npm- . , , 100 .

, , URL /admin , <AdminPage> . Webpack , import AdminPage from './AdminPage.js' .

. , , import('./AdminPage.js') , Webpack , .

, .

, , AdminPage , , URL /admin . , :

 import React from 'react'; class AdminPageLoader extends React.PureComponent { constructor(props) {   super(props);   this.state = {     AdminPage: null,   } } componentDidMount() {   import('./AdminPage').then(module => {     this.setState({ AdminPage: module.default });   }); } render() {   const { AdminPage } = this.state;   return AdminPage     ? <AdminPage {...this.props} />     : <div>Loading...</div>; } } export default AdminPageLoader; 

. (, URL /admin ), ./AdminPage.js , .

render() , <AdminPage> , <div>Loading...</div> , <AdminPage> , .

, react-loadable , React .

Sumário


, , (, , CSS). :

  • — .
  • , — .

Caros leitores! ?

Source: https://habr.com/ru/post/pt423483/


All Articles