Melhoria no desempenho do carregamento de páginas do Next.js e do Gatsby com agrupamento granular

Uma nova estratégia de divisão de webpack no Next.js e no Gatsby minimiza o código duplicado para melhorar o desempenho do carregamento da página.

O Chrome está colaborando com ferramentas e no ecossistema de código aberto JavaScript. Várias otimizações mais recentes foram feitas recentemente foi adicionado para melhorar o desempenho de carregamento da Next.js e Gatsby. Este artigo aborda uma estratégia aprimorada de divisão granular que agora é enviado por padrão nos dois frameworks.

Introdução

Como muitos frameworks da Web, o Next.js e o Gatsby usam o webpack como núcleo bundler. O webpack v3 foi lançado CommonsChunkPlugin para que seja possível módulos de saída compartilhados entre diferentes pontos de entrada em um ou poucos "comuns" bloco (ou blocos). Códigos compartilhados podem ser baixados separadamente e armazenados no cache do navegador antecipadamente, o que pode resultam em um melhor desempenho de carregamento.

Esse padrão se tornou popular com muitas estruturas de aplicativos de página única adotando um ponto de entrada e de pacote com esta aparência:

Configuração comum de pacote e ponto de entrada

Embora seja prático, o conceito de agrupar todo o código do módulo compartilhado em um único bloco tem sua limitações. É possível fazer o download de módulos não compartilhados em todos os pontos de entrada para trajetos que não os utilizam. resultando em downloads de mais códigos do que o necessário. Por exemplo, quando page1 é carregado no bloco common, ele carrega o código para moduleC mesmo que page1 não use moduleC. Por esse motivo, junto com alguns outros, o webpack v4 removeu o plug-in em favor de um novo um: SplitChunksPlugin.

Divisão aprimorada

As configurações padrão de SplitChunksPlugin funcionam bem para a maioria dos usuários. Vários blocos divididos são criada de acordo com uma série de condições para evitar a busca de código duplicado em várias rotas.

No entanto, muitos frameworks da Web que usam esse plug-in ainda seguem um modelo "único comum" abordagem de bloco divisão. O Next.js, por exemplo, geraria um pacote commons que continha qualquer módulo usadas em mais de 50% das páginas e em todas as dependências de framework (react, react-dom e assim por diante).

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 de framework em um bloco compartilhado significa que ele pode ser baixado e armazenados em cache para qualquer ponto de entrada, a heurística baseada em uso de incluir módulos comuns usados em mais de metade das páginas não é muito eficaz. A modificação dessa proporção só resultaria em um destes dois resultados:

  • Se você reduzir a proporção, será feito o download de mais códigos desnecessários.
  • Se você aumentar a proporção, mais códigos vão ser duplicados em vários trajetos.

Para resolver esse problema, a Next.js adotou uma opção diferente padrão paraSplitChunksPlugin que reduz código desnecessário para qualquer trajeto.

  • Qualquer módulo de terceiros grande o suficiente (maior que 160 KB) é dividido em módulos individuais parte
  • Um bloco frameworks separado é criado para as dependências do framework (react, react-dom e assim por diante)
  • Quantos blocos compartilhados forem criados, conforme necessário (até 25)
  • O tamanho mínimo para que um bloco seja gerado é alterado para 20 KB

Essa estratégia de divisão granular oferece os seguintes benefícios:

  • Melhorias no tempo de carregamento da página. Emitindo vários blocos compartilhados, em vez de apenas um, minimiza a quantidade de código desnecessário (ou duplicado) para qualquer ponto de entrada.
  • Armazenamento em cache aprimorado durante as navegações. Dividir bibliotecas grandes e dependências de framework em blocos separados, o que reduz a possibilidade de invalidação do cache, pois é improvável que ambos mudança até que um upgrade seja feito.

Você pode consultar toda a configuração que o Next.js adotou no webpack-config.ts.

Mais solicitações HTTP

SplitChunksPlugin definiu a base para a divisão granular e aplica essa abordagem a uma como Next.js não era um conceito totalmente novo. No entanto, muitas estruturas continuaram usam uma única heurística e "comuns" estratégia de pacotes 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). Portanto, minimizar o número de blocos gerados por um bundler pode garantir que o número total de solicitações permanece abaixo desse limite. No entanto, isso vale apenas para HTTP/1.1. Multiplexação em 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 blocos emitido pelo nosso bundler.

Todos os principais navegadores são compatíveis com HTTP/2. Equipes do Chrome e Next.js queria saber se aumentaria o número de solicitações dividindo os "comuns" únicos do Next.js pacote em vários blocos compartilhados afetaria de alguma forma o desempenho do carregamento. Eles começaram medindo desempenho de um único site enquanto modifica o número máximo de solicitações paralelas usando o método maxInitialRequests .

Desempenho de carregamento de página com maior número de solicitações

Em uma média de três execuções de vários testes em uma única página da Web, o load, start-render e First Contentful Paint permaneceram iguais ao variar o valor inicial máximo contagem de solicitações (de 5 a 15). Curiosamente, notamos uma leve sobrecarga no desempenho, depois de dividir agressivamente centenas de solicitações.

Desempenho de carregamento de página com centenas de solicitações

Isso mostrou que ficar abaixo de um limite confiável (20 a 25 solicitações) era o equilíbrio certo. entre o desempenho de carregamento e a eficiência do armazenamento em cache. Após alguns testes de referência, 25 foram a contagem de maxInitialRequest.

Modificar o número máximo de solicitações que ocorrem em paralelo resultou em mais de uma em um pacote compartilhado, e a separação em cada ponto de entrada reduziu significativamente de código desnecessário para a mesma página.

Reduções de payload de JavaScript com maior divisão

O objetivo do experimento era modificar o número de solicitações para verificar se havia alguma impacto negativo no desempenho do carregamento da página. Os resultados sugerem que definir maxInitialRequests como 25 na página de teste era ideal porque reduziu o tamanho do payload do JavaScript sem desacelerar para baixo na página. A quantidade total de JavaScript necessária para hidratar a página ainda permaneceu Isso explica por que o desempenho do carregamento da página não melhorou necessariamente com a redução de código.

O webpack usa 30 KB como o tamanho mínimo padrão para que um bloco seja gerado. No entanto, o acoplamento de uma O valor de maxInitialRequests de 25 com um tamanho mínimo de 20 KB resultou em melhor armazenamento em cache.

Reduções de tamanho com blocos granulares

Muitos frameworks, incluindo o Next.js, dependem do roteamento do lado do cliente (gerenciado pelo JavaScript) para injetar tags de script mais recentes para cada transição de rota. Mas como eles predeterminam esses blocos dinâmicos no tempo de build?

Next.js usa um arquivo de manifesto de build do lado do servidor para determinar quais blocos gerados são usados diferentes pontos de entrada. Para fornecer essas informações ao cliente também, um resumo do lado do cliente arquivo de manifesto do build foi criado para mapear todas as dependências para 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}`)) || []
  )
}
Saída de vários blocos compartilhados em um aplicativo Next.js.

Essa nova estratégia de divisão granular foi lançada primeiro no Next.js atrás de uma flag, onde foi testada em um de usuários iniciais. Muitos notaram reduções significativas no total de JavaScript usado em seus site inteiro:

.
Site Alteração 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%
Reduções de tamanho do JavaScript: em todas as rotas (compactadas)

A versão final foi enviada por padrão na versão 9.2.

Gatsby

O Gatsby costumava seguir a mesma abordagem de uma heurística 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 de divisão granular semelhante, eles também notamos reduções consideráveis de JavaScript em muitos sites grandes:

.
Site Alteração 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%
Reduções de tamanho do JavaScript: em todas as rotas (compactadas)

Confira o RP para entender como eles implementaram essa lógica na configuração de webpack, que é fornecida por padrão na v2.20.7.

Conclusão

O conceito de envio de blocos granulares não é específico para Next.js, Gatsby ou mesmo webpack. Todos devem considerar melhorar a estratégia de divisão do aplicativo caso ela siga um grande "comum" abordagem de pacote, independente do framework ou do bundler de módulo usado.

  • Se você quiser ver as mesmas otimizações de divisão aplicadas a um aplicativo básico do React, confira este exemplo de React app. Ele usa uma versão simplificada da estratégia de divisão granular e pode ajudar você a aplicar a mesma estratégia de lógica para seu site.
  • Em Rollup, os blocos são criados granular por padrão. Analise manualChunks se quiser fazer isso manualmente e configurar o comportamento.