Amanda Nunes
Voltar aos projetos

De monólito client-side a uma arquitetura orientada por páginas

Como uma evolução arquitetural em duas fases reduziu o First Load JS entre 66% e 83%, dependendo da rota, isolou tenants e recuperou a experiência de desenvolvimento de uma plataforma multi-tenant.

Stack
Next.js 14 · TypeScript · App Router · CMS
Ano
2025
Papel
Investigação e implementação
Contexto
Plataforma multi-tenant, +10 marcas

Contexto

A plataforma atende mais de 10 marcas a partir de uma única base de código, com páginas e layouts definidos via CMS. Apesar de utilizar o Next.js 14 com App Router, imports estáticos, limites de cliente amplos demais e um grande mapa central que referenciava antecipadamente todos os componentes disponíveis faziam a arquitetura se comportar como um monólito client-side.

O desafio

Praticamente todas as rotas apresentavam 1,31 MB de First Load JS, inclusive páginas simples: componentes de outros tenants, fluxos completos e bibliotecas client-only entravam no carregamento inicial. Cache e infraestrutura otimizada mascaravam o efeito no tempo percebido em produção, mas o JavaScript chegava ao dispositivo do usuário de qualquer forma. Em desenvolvimento não havia máscara — o cold compile levava de 40 a 70 segundos, com recompilações amplas e hot reload inconsistente.

1.31MB

First Load JS · por rota

Diagnóstico

O Bundle Analyzer mostrou o mesmo First Load JS em quase todas as rotas, enquanto o JavaScript compartilhado pelo framework somava apenas 87,7 KB. O contraste localizou o problema: não era o Next.js, era uma árvore client-side ampla demais, sem isolamento por tenant e sem code splitting efetivo — o mapa centralizado fazia o template conhecer os componentes de todas as páginas, mesmo quando apenas uma rota era necessária.

Uma evolução em duas fases

A primeira intervenção atacou o tamanho do bundle; a segunda tornou a resolução de páginas previsível e sustentável.

Fase 1 — Code splitting orientado pelo CMS

Reestruturei o carregamento das páginas com next/dynamic para que apenas os componentes solicitados pelo CMS fossem carregados. A mudança criou chunks por página e isolou os tenants entre si — componentes de outras marcas deixaram de participar do carregamento inicial — além de estabelecer limites mais claros entre componentes de servidor e de cliente. Também retirei do bundle e do Data Cache um arquivo de mapeamento de URLs de aproximadamente 80 MB, migrei as notificações de react-toastify para Sonner e reduzi o middleware.

Fase 2 — Resolução orientada por páginas

Numa evolução posterior, removi o mapeamento centralizado e introduzi resolvers determinísticos no servidor: cada rota passou a selecionar explicitamente sua página. Isso fortaleceu a separação entre servidor e cliente e corrigiu os full reloads indevidos e as recompilações globais que haviam permanecido após a primeira fase.

  1. 01Code splitting real com next/dynamic
  2. 02Carregamento sob demanda a partir do CMS
  3. 03Isolamento efetivo entre tenants
  4. 04Limites de servidor e cliente menores e explícitos
  5. 05Reorganização controlada dos imports de ícones
  6. 06Mapeamento de URLs removido do bundle e do Data Cache
  7. 07Migração de react-toastify para Sonner
  8. 08Middleware reduzido no edge
  9. 09Remoção dos mapas centralizados
  10. 10Resolvers server-side orientados por página
Resolução de páginas: antes, fase 1 e fase 2Antes, um mapa estático global se conectava a todas as páginas disponíveis, inclusive as que a rota atual não usa. Na fase 1, um mapa dinâmico alimentado pelo CMS passou a carregar apenas os componentes da página pedida. Na fase 2, um resolver no servidor passou a selecionar essa página de forma determinística.ANTESMapa estático globalOutras páginasPágina atualOutras páginasOutras páginasFASE 1Mapa dinâmico + CMSPágina atualFASE 2Resolver server-sidePágina atual

Resultados

Antes da refatoração, praticamente todas as rotas apresentavam aproximadamente 1,31 MB de First Load JS. Depois da implementação do code splitting orientado por páginas, os valores passaram a variar entre cerca de 220 e 450 KB por rota, representando reduções aproximadas de 66% a 83%, dependendo da página. O JavaScript compartilhado entre as rotas ficou em aproximadamente 91,8 KB. O ganho apareceu também no ciclo de trabalho: as recompilações globais deram lugar a rebuilds incrementais, e mudanças localizadas passaram a produzir hot reload em torno de 2 segundos.

First Load JS · por rota
antes1.31 MB
depois220–450 KB
JavaScript compartilhado
91,8 KB
Middleware
antes 64.4 KB, depois 27.7 KB
Cold compile
antes 40–70 s, depois Rebuild incremental
Hot reload
~2 s
Plataforma
+10 marcas

Minha atuação

Conduzi individualmente a investigação e a implementação das duas fases: analisei bundles e logs, identifiquei os gargalos, implementei o carregamento dinâmico, reorganizei os limites entre servidor e cliente, retirei dependências pesadas, otimizei o middleware e evoluí a resolução para o modelo orientado por páginas. A liderança técnica discutiu os trade-offs, revisou e aprovou os PRs.

Uma decisão que permaneceu

Na concepção da plataforma, também propus Zustand como alternativa aos contexts globais que haviam crescido sem limites claros nos projetos legados. Uma prova de conceito aplicada a um estado real ajudou a equipe a avaliar os trade-offs. Hoje, a solução é a abordagem preferida do time para estado global.

Usar um framework moderno não garante uma arquitetura moderna.
Performance também é experiência de desenvolvimento. Bundles previsíveis, hot reload confiável e isolamento por rota afetam diretamente a capacidade da equipe de evoluir um produto com segurança.

Este estudo descreve minha contribuição sem revelar código, telas, clientes ou regras de negócio proprietárias.