Uma estratégia mais recente de divisão de pacotes do webpack no Next.js e no Gatsby minimiza o código duplicado para melhorar o desempenho de carregamento da página.
O Chrome está colaborando com ferramentas e estruturas no ecossistema de código aberto do JavaScript. Várias otimizações mais recentes foram adicionadas recentemente para melhorar o desempenho de carregamento do Next.js e do Gatsby. Este artigo aborda uma estratégia granular aprimorada de divisão em partes que agora é enviada por padrão nos dois frameworks.
Introdução
Assim como muitos frameworks da Web, o Next.js e o Gatsby usam o webpack como bundler principal. O webpack v3 introduziu o CommonsChunkPlugin
para permitir a saída de módulos compartilhados entre diferentes pontos de entrada em um único (ou poucos) bloco "commons" (ou blocos). O código compartilhado pode ser baixado separadamente e armazenado no cache do navegador no início, o que pode resultar em um melhor desempenho de carregamento.
Esse padrão se tornou popular com muitas frameworks de aplicativos de página única adotando um ponto de entrada e uma configuração de pacote semelhante a esta:
Embora seja prático, o conceito de agrupar todo o código do módulo compartilhado em um único bloco tem limitações. Os módulos não compartilhados em todos os pontos de entrada podem ser baixados para rotas que não os usam, resultando no download de mais código do que o necessário. Por exemplo, quando page1
carrega o trecho common
, ele carrega o código de moduleC
, mesmo que page1
não use moduleC
.
Por esse motivo, entre outros, o webpack v4 removeu o plug-in em favor de um novo: SplitChunksPlugin
.
Melhoria no chunking
As configurações padrão para SplitChunksPlugin
funcionam bem para a maioria dos usuários. Vários trechos divididos são criados dependendo de várias condições para evitar a busca de código duplicado em várias rotas.
No entanto, muitas estruturas da Web que usam esse plug-in ainda seguem uma abordagem de divisão de
chunks "single-commons". O Next.js, por exemplo, geraria um pacote commons
que continha qualquer módulo usado em mais de 50% das páginas e todas as dependências do framework (react
, react-dom
etc.).
const splitChunksConfigs = {
…
prod: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
},
react: {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
},
},
},
Embora incluir código dependente da estrutura em um trecho compartilhado signifique que ele pode ser baixado e armazenado em cache para qualquer ponto de entrada, a heurística baseada no uso de incluir módulos comuns usados em mais da metade das páginas não é muito eficaz. Modificar essa proporção só resultaria em um de dois resultados:
- Se você reduzir a proporção, mais código desnecessário será baixado.
- Se você aumentar a proporção, mais código será duplicado em várias rotas.
Para resolver esse problema, o Next.js adotou uma configuração
diferente paraSplitChunksPlugin
que reduz
o código desnecessário de qualquer rota.
- Qualquer módulo de terceiros suficientemente grande (mais de 160 KB) é dividido em um bloco individual.
- Um bloco
frameworks
separado é criado para dependências de framework (react
,react-dom
e assim por diante). - São criados quantos blocos compartilhados forem necessários (até 25).
- O tamanho mínimo para gerar um trecho foi alterado para 20 KB
Essa estratégia granular de divisão em partes oferece os seguintes benefícios:
- O tempo de carregamento da página é reduzido. Emitir vários blocos compartilhados, em vez de um único, minimiza a quantidade de código desnecessário (ou duplicado) para qualquer ponto de entrada.
- Melhoria no armazenamento em cache durante a navegação. Dividir grandes bibliotecas e dependências de framework em partes separadas reduz a possibilidade de invalidação do cache, já que é improvável que ambos mudem até que uma atualização seja feita.
Você pode conferir toda a configuração adotada pelo Next.js em webpack-config.ts
.
Mais solicitações HTTP
O SplitChunksPlugin
definiu a base para o chunking granular, e aplicar essa abordagem a um framework como o Next.js não era um conceito totalmente novo. No entanto, muitos frameworks ainda usam uma única heurística e uma estratégia de pacote "commons" por alguns motivos. Isso inclui a preocupação de que
muito mais solicitações HTTP podem afetar negativamente o desempenho do site.
Os navegadores só podem abrir um número limitado de conexões TCP para uma única origem (seis para o Chrome). Por isso, minimizar o número de partes geradas por um bundler garante que o número total de solicitações fique abaixo desse limite. No entanto, isso só é válido para HTTP/1.1. A multiplexagem no HTTP/2 permite que várias solicitações sejam transmitidas em paralelo usando uma única conexão em uma única origem. Em outras palavras, geralmente não precisamos nos preocupar em limitar o número de partes emitidas pelo nosso pacote.
Todos os principais navegadores são compatíveis com HTTP/2. As equipes do Chrome e do Next.js queriam saber se o aumento do número de solicitações dividindo o pacote único "commons" do Next.js em vários blocos compartilhados afetaria o desempenho do carregamento de alguma forma. Eles começaram medindo a performance de um único site e modificando o número máximo de solicitações paralelas usando a propriedade maxInitialRequests
.
Em uma média de três execuções de vários testes em uma única página da Web, os tempos de load
, start-render e First Contentful Paint permaneceram aproximadamente os mesmos ao variar a contagem máxima de solicitações iniciais (de 5 a 15). Curiosamente, notamos uma pequena sobrecarga de desempenho somente depois de dividir agressivamente em centenas de solicitações.
Isso mostrou que ficar abaixo de um limite confiável (20 a 25 solicitações) estabeleceu o equilíbrio certo entre o desempenho de carregamento e a eficiência do armazenamento em cache. Depois de alguns testes de referência, 25 foi selecionado como a contagem de maxInitialRequest
.
A modificação do número máximo de solicitações paralelas resultou em mais de um pacote compartilhado. A separação adequada para cada ponto de entrada reduziu significativamente a quantidade de código desnecessário para a mesma página.
O experimento consistiu apenas em modificar o número de solicitações para verificar se haveria algum efeito negativo no desempenho do carregamento da página. Os resultados sugerem que definir maxInitialRequests
como 25
na página de teste foi ideal porque reduziu o tamanho do payload JavaScript sem diminuir a velocidade da página. A quantidade total de JavaScript necessária para hidratar a página permaneceu aproximadamente a mesma, o que explica por que o desempenho de carregamento da página não melhorou necessariamente com a quantidade reduzida de código.
O webpack usa 30 KB como um tamanho mínimo padrão para gerar um trecho. No entanto, combinar um valor de maxInitialRequests
de 25 com um tamanho mínimo de 20 KB resultou em um armazenamento em cache melhor.
Reduções de tamanho com partes granulares
Muitos frameworks, incluindo o Next.js, usam o roteamento do lado do cliente (processado por JavaScript) para injetar tags de script mais recentes em cada transição de rota. Mas como eles predeterminam esses blocos dinâmicos no momento da build?
O Next.js usa um arquivo de manifesto de build do lado do servidor para determinar quais partes geradas são usadas por diferentes pontos de entrada. Para fornecer essas informações ao cliente também, um arquivo de manifesto de build abreviado do lado do cliente foi criado para mapear todas as dependências de cada ponto de entrada.
// Returns a promise for the dependencies for a particular route
getDependencies (route) {
return this.promisedBuildManifest.then(
man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
)
}

Essa nova estratégia granular de divisão em partes foi lançada primeiro no Next.js por trás de uma flag, onde foi testada em vários usuários pioneiros. Muitos tiveram reduções significativas no total de JavaScript usado em todo o site:
Site | Mudança total de JS | Diferença % |
---|---|---|
https://www.barnebys.com/ | -238 KB | -23% |
https://sumup.com/ | -220 KB | -30% |
https://www.hashicorp.com/ | -11 MB | -71% |
A versão final foi enviada por padrão na versão 9.2.
Gatsby
O Gatsby costumava seguir a mesma abordagem de usar uma heurística baseada no uso para definir módulos comuns:
config.optimization = {
…
splitChunks: {
name: false,
chunks: `all`,
cacheGroups: {
default: false,
vendors: false,
commons: {
name: `commons`,
chunks: `all`,
// if a chunk is used more than half the components count,
// we can assume it's pretty global
minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
},
react: {
name: `commons`,
chunks: `all`,
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
Ao otimizar a configuração do webpack para adotar uma estratégia granular semelhante de divisão em partes, eles também notaram reduções consideráveis de JavaScript em muitos sites grandes:
Site | Mudança total de JS | Diferença % |
---|---|---|
https://www.gatsbyjs.org/ | -680 KB | -22% |
https://www.thirdandgrove.com/ | -390 KB | -25% |
https://ghost.org/ | -1,1 MB | -35% |
https://reactjs.org/ | -80 Kb | -8% |
Confira o PR para entender como eles implementaram essa lógica na configuração do webpack, que é enviada por padrão na v2.20.7.
Conclusão
O conceito de envio de partes granulares não é específico do Next.js, do Gatsby ou até mesmo do webpack. Todos devem considerar melhorar a estratégia de divisão em partes do aplicativo se ela seguir uma abordagem de pacote "commons" grande, independente da estrutura ou do pacote de módulos usado.
- Se quiser ver as mesmas otimizações de divisão em partes aplicadas a um aplicativo React simples, confira este exemplo de app React (link em inglês). Ele usa uma versão simplificada da estratégia de divisão granular em partes e pode ajudar você a começar a aplicar o mesmo tipo de lógica ao seu site.
- No Rollup, os blocos são criados de forma granular por padrão. Consulte
manualChunks
se quiser configurar o comportamento manualmente.