Next.js
Performance
Cache
Arquitetura
React

Cache e streaming no Next.js: performance virou decisão de arquitetura

Performance percebida deixou de ser ajuste final e passou a ser uma escolha de arquitetura, com ganhos reais e o risco do dado velho.

Cache e streaming no Next.js: performance virou decisão de arquitetura

Por muito tempo, performance foi a última etapa do projeto. Construía-se a aplicação, media-se no fim, e quando a página estava lenta saíam os paliativos: um cache aqui, um spinner ali, um lazy load acolá. Era um detalhe de acabamento, tratado depois que as decisões importantes já tinham sido tomadas.

O Next.js moderno desmonta essa ordem. Cache, streaming e Suspense não são botões que você liga no final; são propriedades de como a página é montada e entregue. Decidir quando um dado é recalculado, em que ordem as partes da tela aparecem e o que pode ser servido antes do resto são escolhas estruturais. A tese deste texto é simples: performance percebida virou decisão de arquitetura, e tratá-la como detalhe final passou a custar caro.

Três mecanismos que resolvem problemas diferentes

Vale separar o que cada peça faz, porque elas costumam ser confundidas em uma só ideia vaga de "deixar rápido".

Cache trata de não refazer trabalho. Se um dado já foi buscado ou uma página já foi renderizada, guardá-los evita pagar o mesmo custo de novo na próxima requisição. O ganho é de throughput e de latência: respostas que não precisam tocar o banco saem em frações do tempo.

Streaming trata de não esperar tudo ficar pronto para começar a entregar. Em vez de segurar a página inteira até que a parte mais lenta termine, o servidor manda o HTML em pedaços, conforme cada um fica disponível. O usuário começa a ver e a interagir com o que já chegou enquanto o resto ainda está sendo montado.

Suspense é o que torna o streaming utilizável. Ele permite declarar, no próprio componente, "enquanto este dado não chega, mostre isto". Marca as fronteiras entre o que já está pronto e o que ainda carrega, dando ao framework permissão para enviar a tela em partes coerentes em vez de um bloco único.

Como eles trabalham juntos

A mágica aparece na combinação. Imagine uma página de produto: cabeçalho, dados do item, avaliações e recomendações. O cabeçalho e os dados do item são rápidos. As avaliações dependem de uma agregação pesada. As recomendações chamam um serviço externo lento.

No modelo antigo, a página inteira esperava o componente mais lento. O usuário olhava uma tela em branco até que tudo, inclusive a recomendação que veio de um terceiro mal-humorado, terminasse. A performance da página era refém do seu pior elemento.

Com Suspense delimitando cada bloco e streaming ativo, o servidor entrega na hora o cabeçalho e os dados do item, com indicadores de carregamento no lugar das avaliações e das recomendações. Conforme cada parte fica pronta no servidor, ela é transmitida e encaixa no lugar. Por baixo, o cache garante que, na próxima visita, as partes que não mudaram nem precisem ser recalculadas. Os três mecanismos somam: cache reduz o trabalho, streaming remove a espera pelo pior caso, Suspense organiza a entrega.

O resultado é que a performance percebida deixa de depender do componente mais lento e passa a depender de como você desenhou as fronteiras. E desenhar fronteiras é arquitetura.

Por que isso vira decisão de arquitetura, não ajuste final

Repare que cada escolha acima foi tomada cedo, não no fim. Onde colocar um limite de Suspense, qual dado pode esperar e qual precisa estar no primeiro byte, o que é cacheável e por quanto tempo: tudo isso molda a estrutura de componentes e a forma como os dados são buscados.

Você não consegue "adicionar streaming depois" em uma página que foi escrita como um bloco monolítico que busca tudo de uma vez. Para streamar, a página precisa ter sido pensada em partes independentes, cada uma com sua própria fronteira de carregamento. Essa decomposição é uma decisão de design que acontece no começo, junto com a modelagem dos dados.

O mesmo vale para o cache. Decidir o que pode ser servido de uma versão guardada e o que precisa estar sempre fresco é, na prática, classificar os dados do seu domínio por tolerância a desatualização. Isso não é um ajuste de performance, é uma afirmação sobre o negócio: este preço pode estar alguns minutos velho? Este saldo, não? Essas respostas pertencem à arquitetura, e quem as adia para o fim descobre que reescrever a estrutura de busca de dados é bem mais caro do que tê-la pensado antes. A relação com o resto do framework fica mais clara no guia do App Router do Next.js.

O risco que ninguém menciona no slide bonito: cache mal entendido

Cache é a parte mais sedutora e a mais perigosa. A frase clássica de que invalidação de cache é um dos problemas difíceis da computação não é piada de programador, é descrição de produção.

O problema central é o dado velho. No momento em que você decide guardar uma resposta, você aceita que ela pode estar desatualizada quando alguém a ler de novo. Para conteúdo que muda devagar, ótimo. Para um saldo, um estoque, um status de pedido, servir uma versão guardada por tempo demais significa mostrar ao usuário uma realidade que já não existe. E o pior tipo desse bug é o silencioso: nada quebra, nada dá erro, a tela simplesmente mente.

O Next.js oferece controles finos de cache, justamente porque essas decisões precisam ser por dado, não globais. Mas controle fino é faca de dois gumes: o desenvolvedor que não entende exatamente em qual camada um dado está sendo guardado vai depurar comportamentos fantasmas. Por que esta página não atualiza? Porque há uma camada de cache que ele esqueceu que existia, com uma chave de invalidação que ninguém disparou. Quem quiser se aprofundar encontra um bom apanhado em boas práticas de cache em aplicações.

A complexidade é parte do trato

Há um custo cognitivo que precisa entrar na conta honestamente. Com várias camadas de cache, streaming e fronteiras de Suspense, o modelo mental de "o que está acontecendo quando o usuário pede esta página" fica mais rico, e mais difícil de manter na cabeça.

Um dado pode estar guardado em mais de um nível, com tempos de vida diferentes. Uma parte da página renderiza no servidor e transmite, outra hidrata no cliente. Quando algo aparece desatualizado, a investigação precisa percorrer essas camadas para descobrir onde a versão velha ficou presa. Isso exige que o time entenda o modelo, e não apenas copie configurações de um exemplo da internet.

Por isso a recomendação prática é ser explícito e conservador. Comece com menos cache do que parece tentador e adicione camadas quando a medição justificar, documentando a estratégia de invalidação de cada uma. Trate cache agressivo em dado sensível como decisão que precisa de justificativa, não como padrão. A regra de ouro é que ninguém deveria conseguir explicar por que uma tela mostra informação velha apenas dando de ombros.

O que levar para a decisão

Cache, streaming e Suspense, juntos, deixaram a performance percebida bem melhor do que era possível com os truques de acabamento da geração anterior. O ganho é concreto: páginas que aparecem em partes, trabalho que não se repete, esperas que não travam o usuário no pior caso.

O contraponto é que esses ganhos vêm de decisões tomadas cedo, sobre fronteiras de componentes e tolerância a desatualização dos dados. Adiá-las para o fim não é neutro: é abrir mão do streaming e empurrar o cache para o território do dado velho silencioso. Performance percebida virou arquitetura, com tudo o que isso implica de planejamento e de disciplina de invalidação.

Se você está definindo a stack de uma aplicação Next.js e quer evitar o pesadelo do cache que ninguém entende, vale desenhar a estratégia de dados antes da primeira tela. Tenho discutido bastante esse desenho; me encontre nos comentários ou nas redes para trocar ideia.

Leia também

Cache e streaming no Next.js: performance virou decisão de arquitetura | Matheus Breguêz