Capa do Ebook gratuito Performance Front-End: Otimizando Core Web Vitals sem Mistério

Performance Front-End: Otimizando Core Web Vitals sem Mistério

Novo curso

19 páginas

Divisão de bundles e carregamento sob demanda com code splitting

Capítulo 12

Tempo estimado de leitura: 13 minutos

+ Exercício

Dividir bundles e carregar código sob demanda (code splitting) é uma estratégia para reduzir o volume de JavaScript entregue no carregamento inicial e adiar partes do app para o momento em que realmente são necessárias. Em vez de enviar um único arquivo grande com tudo (rotas, componentes raros, bibliotecas pesadas, painéis administrativos, editores, gráficos), você entrega um “núcleo” mínimo para a primeira tela e cria “pedaços” (chunks) que são baixados quando o usuário navega, abre um modal, acessa uma rota específica ou ativa um recurso.

Na prática, code splitting atua em duas frentes: (1) diminuir o custo de download/parse/compile/execução do JavaScript inicial e (2) melhorar o cache, porque mudanças em uma parte do app não invalidam o bundle inteiro. O resultado costuma ser uma experiência mais rápida no primeiro acesso e mais estável em deploys frequentes, desde que o carregamento sob demanda seja planejado para não criar “buracos” de UX (telas vazias, atrasos perceptíveis ao clicar) e não gerar explosão de requests pequenos demais.

O que é um bundle e por que ele cresce

Bundle é o arquivo (ou conjunto de arquivos) gerado pelo seu build (Webpack, Vite/Rollup, esbuild etc.) que agrupa módulos JavaScript e seus imports. Ele cresce por motivos comuns: dependências adicionadas ao longo do tempo, componentes reutilizados em múltiplas rotas, bibliotecas utilitárias importadas de forma não otimizada, e features raras (ex.: editor WYSIWYG, mapas, dashboards) que acabam indo junto no “pacote principal”.

Quando tudo vai para o bundle inicial, o navegador precisa baixar mais bytes, fazer parse/compilação de mais código e executar mais inicialização. Mesmo que parte desse código não seja usada na primeira tela, ele ainda pode custar tempo de CPU e memória. Code splitting tenta alinhar “código entregue” com “código necessário agora”.

Tipos de code splitting (e quando usar)

1) Por rota (route-based splitting)

Divide o app por páginas/rotas. É o caso mais comum: a rota “/checkout” não precisa do código de “/admin”. Em SPAs e apps com roteamento no cliente, isso reduz bastante o JS inicial. Em frameworks com roteamento por arquivos, normalmente há suporte nativo para dividir por rota.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

2) Por componente (component-based splitting)

Carrega componentes pesados apenas quando aparecem. Exemplo: um gráfico só aparece ao expandir uma seção; um editor rico só aparece ao clicar em “Editar”. Isso evita pagar o custo de bibliotecas grandes para usuários que nunca acionam o recurso.

3) Por feature/condição (conditional splitting)

Carrega código dependendo de permissões, flags, AB tests, tipo de dispositivo ou contexto. Exemplo: recursos de administração só para usuários com role “admin”.

4) Por vendor (vendor splitting)

Separa dependências de terceiros (react, date-fns, charting libs) do seu código. A vantagem é cache: seu app muda com frequência, mas vendors mudam menos. A desvantagem é que “vendor” pode virar um chunk gigante se não houver controle. Em bundlers modernos, a divisão automática pode ser suficiente, mas vale revisar.

Trade-offs: o que pode dar errado

  • Excesso de chunks pequenos: muitos arquivos aumentam overhead de requests e podem piorar em redes ruins. HTTP/2 ajuda, mas não elimina custo de handshake, prioridade e congestionamento.

  • Latência ao interagir: se o usuário clica e o chunk ainda não está em cache, a UI pode travar esperando download/parse. Isso afeta a sensação de responsividade.

  • Duplicação de código: imports compartilhados podem ser copiados em múltiplos chunks se a configuração não extrair “common chunks”. Isso aumenta bytes totais.

  • Cache invalidado com frequência: se o chunk “comum” é grande e muda sempre, você perde o benefício do cache. É importante manter chunks estáveis e com hashing no nome.

  • Pré-carregamento agressivo: se você “prefetch” tudo, volta ao problema original (baixar mais do que precisa).

Passo a passo prático: implementando code splitting com import()

O mecanismo básico do code splitting em JavaScript moderno é o import() dinâmico. Ele instrui o bundler a criar um chunk separado e o navegador a baixar esse chunk apenas quando a linha for executada.

Passo 1: identificar um candidato “pesado” e pouco frequente

Escolha algo que não seja necessário no carregamento inicial: um modal de ajuda, um editor, um componente de gráficos, um mapa, um painel avançado. O ideal é que o usuário só acione em uma interação clara (clique) ou em uma rota específica.

Passo 2: trocar import estático por import dinâmico

Suponha um componente de gráfico:

// Antes (import estático): entra no bundle inicial
import { ChartPanel } from './ChartPanel';

export function Dashboard() {
  return <ChartPanel />;
}

Agora com import dinâmico (exemplo genérico, sem framework específico):

// Depois: carrega sob demanda
async function loadChartPanel() {
  const module = await import('./ChartPanel');
  return module.ChartPanel;
}

export async function openChart(container) {
  const ChartPanel = await loadChartPanel();
  // renderize ChartPanel no container conforme sua stack
}

Em React, o padrão comum é React.lazy + Suspense:

import React, { Suspense } from 'react';

const ChartPanel = React.lazy(() => import('./ChartPanel'));

export function Dashboard() {
  return (
    <Suspense fallback={<div>Carregando gráfico...</div>}>
      <ChartPanel />
    </Suspense>
  );
}

O bundler cria um chunk para ChartPanel (e suas dependências). Esse chunk só é baixado quando o componente for renderizado.

Passo 3: garantir um fallback que não “quebre” a UI

Carregamento sob demanda precisa de um estado de carregamento. O fallback deve ser leve e coerente: skeleton, spinner discreto, placeholder com altura fixa (para evitar saltos visuais). Se o componente ocupa uma área grande, reserve o espaço para não causar deslocamentos.

Passo 4: tratar erros de carregamento (rede/offline)

Chunks podem falhar ao baixar (rede instável, usuário offline, cache corrompido). Trate erros para não deixar a tela travada:

const ChartPanel = React.lazy(() =>
  import('./ChartPanel').catch((err) => {
    // Você pode registrar o erro e retornar um módulo alternativo
    console.error('Falha ao carregar ChartPanel', err);
    return { default: () => <div>Não foi possível carregar o gráfico.</div> };
  })
);

Em stacks sem React.lazy, o princípio é o mesmo: import() dentro de um try/catch e UI de fallback.

Divisão por rota: padrão mais eficiente para SPAs

Para apps com roteamento no cliente, dividir por rota costuma trazer o maior ganho com menor complexidade. A ideia é que cada rota principal tenha seu próprio chunk, e o “shell” do app (layout, navegação, estado mínimo) fique no bundle inicial.

Exemplo com React Router (lazy routes)

import { createBrowserRouter } from 'react-router-dom';
import AppShell from './AppShell';

const Home = () => import('./routes/Home');
const Checkout = () => import('./routes/Checkout');
const Admin = () => import('./routes/Admin');

export const router = createBrowserRouter([
  {
    path: '/',
    element: <AppShell />,
    children: [
      { index: true, lazy: Home },
      { path: 'checkout', lazy: Checkout },
      { path: 'admin', lazy: Admin }
    ]
  }
]);

Esse padrão mantém o carregamento inicial mais leve e distribui o custo conforme a navegação. Para rotas críticas (ex.: checkout), você pode combinar com prefetch em momentos oportunos (ver seção de prefetch).

Estratégias de prefetch/preload para evitar latência ao clicar

Carregar sob demanda reduz o custo inicial, mas pode introduzir atraso quando o usuário aciona uma feature. Para equilibrar, use prefetch/preload de forma seletiva.

Prefetch: “talvez eu precise em breve”

Prefetch é adequado para chunks prováveis, mas não imediatos. Exemplos: ao passar o mouse em um link, ao focar um item de menu, ou após o app ficar ocioso por alguns segundos.

Alguns bundlers suportam hints:

// Webpack magic comment
const Checkout = React.lazy(() => import(/* webpackPrefetch: true */ './routes/Checkout'));

Você também pode implementar prefetch manual acionando import() sem renderizar:

let checkoutPromise;

export function prefetchCheckout() {
  if (!checkoutPromise) checkoutPromise = import('./routes/Checkout');
  return checkoutPromise;
}

// Exemplo: no hover do link
// onMouseEnter={() => prefetchCheckout()}

Preload: “vou precisar agora”

Preload é mais agressivo e deve ser usado com cuidado. É útil quando você sabe que a próxima ação é iminente (ex.: usuário clicou em “Próximo” e você quer adiantar o chunk da próxima etapa enquanto valida dados). Em alguns bundlers:

const Step2 = React.lazy(() => import(/* webpackPreload: true */ './checkout/Step2'));

Evite preload global de muitos chunks, pois compete com recursos críticos.

Separando dependências pesadas: exemplos comuns

Gráficos e visualizações

Bibliotecas de chart costumam ser grandes. Carregue-as apenas em páginas que exibem gráficos ou quando o usuário abre a aba “Relatórios”. Outra alternativa é trocar por versões “core”/modulares (importar apenas tipos de gráfico usados), mas isso já entra em otimização de dependências; aqui o foco é isolar o custo em chunks sob demanda.

Editores ricos (WYSIWYG/Markdown)

Um editor completo pode adicionar centenas de KB. Se a maioria dos usuários só lê conteúdo, carregue o editor apenas ao entrar em modo de edição. Combine com placeholder para manter a área estável enquanto carrega.

Mapas

SDKs de mapas e geocoding são pesados. Carregue somente quando o usuário abrir a seção de mapa. Se houver um preview estático, use-o como fallback e substitua pelo mapa interativo após o chunk carregar.

Configuração de bundlers: como influenciar a divisão de chunks

Além do import(), você pode controlar como o bundler agrupa módulos para evitar duplicação e criar chunks com melhor cache.

Webpack: splitChunks (visão prática)

Uma configuração típica extrai vendors e módulos comuns. Exemplo simplificado:

// webpack.config.js (exemplo)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    },
    runtimeChunk: 'single'
  }
};

runtimeChunk: 'single' ajuda a manter o runtime do Webpack separado, reduzindo invalidações de cache quando chunks mudam. Ajuste com cuidado: um “vendors” gigante pode ser ruim se incluir bibliotecas raras; nesses casos, vale criar cacheGroups mais específicos (ex.: separar charting libs em um chunk próprio).

Vite/Rollup: manualChunks

No Vite (Rollup), você pode agrupar vendors por pacote:

// vite.config.js (exemplo)
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom'],
          router: ['react-router-dom']
        }
      }
    }
  }
};

Use isso para manter chunks estáveis e evitar que uma dependência rara vá parar no chunk principal. Não exagere: muitos chunks “fixos” podem aumentar overhead.

Boas práticas para escolher o que vai para o bundle inicial

  • App shell mínimo: layout, navegação, estado essencial e componentes da primeira tela. Evite incluir páginas internas e features raras.

  • Evite importar “barrels” que puxam tudo: arquivos index.ts que reexportam muitos módulos podem incluir código desnecessário no chunk inicial. Prefira imports diretos quando isso afetar a divisão.

  • Isolar código de admin/experimentos: rotas internas e painéis devem ser chunks separados, com prefetch apenas para usuários elegíveis.

  • Evitar side effects no topo do módulo: se um módulo executa código ao ser importado (polyfills, inicializações globais), ele pode “vazar” para o bundle inicial. Mantenha inicializações pesadas atrás de funções chamadas sob demanda.

Code splitting e cache: como colher o benefício

Para que a divisão em chunks ajude de verdade, o cache precisa funcionar bem. Em geral, isso depende de:

  • Nomes com hash de conteúdo: arquivos como chunk.8f3a1.js mudam apenas quando o conteúdo muda.

  • Headers de cache adequados: assets com hash podem ter cache longo, enquanto o HTML deve ser revalidado com frequência.

  • Separar runtime/manifest: reduz invalidação em cascata quando um chunk muda.

O objetivo é que, em um novo deploy, o usuário baixe apenas os chunks que realmente mudaram, e não tudo novamente.

Como validar se o code splitting está funcionando

1) Inspecionar a saída do build

Verifique quantos chunks foram gerados, seus tamanhos e o que está dentro. Ferramentas como bundle analyzers ajudam a enxergar dependências grandes e duplicações. O ponto principal é confirmar que rotas/features raras não estão no chunk inicial.

2) Verificar a rede ao navegar

Abra o app, carregue a primeira tela e observe quais arquivos JS são baixados. Depois navegue para uma rota “pesada” e confirme que novos chunks são solicitados apenas nesse momento. Se tudo baixa no início, o splitting não está efetivo ou há prefetch agressivo.

3) Medir impacto em cenários reais

Compare: (a) bytes de JS no carregamento inicial, (b) tempo até a primeira interação relevante na tela inicial, e (c) latência ao navegar para rotas sob demanda. O ideal é reduzir (a) sem piorar demais (c). Se (c) piorar, aplique prefetch seletivo para rotas prováveis.

Padrões práticos: receitas aplicáveis

Receita A: modal pesado sob demanda

Você tem um modal “Ajuda” com um player, markdown e busca interna. Ele não precisa estar no bundle inicial.

  • Crie um componente HelpModal isolado.

  • Troque o import estático por React.lazy(() => import('./HelpModal')).

  • Renderize o modal apenas quando isOpen for true.

  • Adicione prefetch no onMouseEnter do botão “Ajuda” se o modal for muito usado.

Receita B: rota de checkout com prefetch no carrinho

Checkout é crítico, mas nem todo usuário chega lá. Uma abordagem equilibrada:

  • Divida a rota de checkout em chunk separado.

  • Quando o usuário abre o carrinho (evento forte de intenção), faça import('./routes/Checkout') em background.

  • Ao clicar em “Finalizar compra”, o chunk provavelmente já estará em cache, reduzindo latência.

Receita C: admin isolado por permissão

Se apenas uma fração dos usuários acessa admin:

  • Não inclua a rota admin no menu para quem não tem permissão.

  • Carregue a rota admin via import dinâmico apenas após confirmar a role.

  • Evite colocar bibliotecas de admin no chunk vendors global; crie um chunk separado (manualChunks/cacheGroups) se necessário.

Armadilhas comuns e como evitar

Importar algo pesado “por acidente” no app shell

Um componente pequeno no header pode importar um utilitário que, por sua vez, importa uma biblioteca grande. Isso puxa peso para o bundle inicial. Solução: rastrear a cadeia de imports e mover a dependência pesada para um ponto sob demanda (ou criar uma versão leve do utilitário).

Chunks duplicados por falta de extração de comuns

Se duas rotas importam o mesmo conjunto de módulos, o bundler pode duplicar parte do código em cada chunk. Solução: habilitar extração de comuns (splitChunks) e revisar cacheGroups/manualChunks para módulos compartilhados.

Prefetch em excesso

Alguns frameworks adicionam prefetch automático para links visíveis. Isso pode ser bom ou ruim dependendo do app. Se você notar que muitos chunks são baixados sem navegação, ajuste a política de prefetch (desabilitar global, limitar a rotas críticas, condicionar a Wi-Fi/idle).

Carregamento sob demanda sem UX de transição

Se o usuário clica e a tela fica branca, a percepção de performance piora. Solução: fallback com skeleton, manter layout estável e, quando fizer sentido, prefetch por intenção (hover/focus) ou por ociosidade.

Agora responda o exercício sobre o conteúdo:

Qual é a principal vantagem de usar code splitting com carregamento sob demanda em um aplicativo front-end?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O code splitting entrega um núcleo mínimo na primeira tela e carrega chunks sob demanda, reduzindo download/parse/execução inicial e melhorando cache, já que mudanças não invalidam todo o bundle.

Próximo capitúlo

Estratégias de cache e validação: HTTP caching, ETag e versionamento de assets

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.