Next.js vs TanStack Start direto na AWS
O que muda de verdade quando o mesmo app SSR roda na mesma infraestrutura da AWS.
Coloque qualquer um dos dois atrás do CloudFront e, de fora, tudo parece igual. A diferença está nos detalhes: onde o seu código roda, como os dados são alterados, quanto tempo um conteúdo novo leva para ir ao ar e quanto trabalho de manutenção sobra para você quando algo quebra.
Next.js via OpenNext
Já traz mais coisa pronta (ISR, otimização de imagens, middleware), mas, para isso, você precisa de mais peças na AWS e de um adaptador da comunidade.
TanStack Start via Nitro
Menos peças na AWS e uma separação mais clara entre servidor e cliente. Em compensação, traz menos recursos prontos, não tem um alvo oficial na AWS e ainda era um release candidate em novembro de 2025.
Tudo aqui tem link para a fonte. As caixas marcadas como Opinião ou Inferência são leitura minha.
- 1. Do merge do PR até a página no navegador
- 2. O que cada framework adiciona
- 3. Onde o seu código roda
- 4. Como o conteúdo fica atualizado
- 5. Estratégia de renderização por rota
- 6. Core Web Vitals
- 7. O que fica com você quando algo quebra
- 8. Indo mais fundo: Lambda e mutations
1. Do merge do PR até a página no navegador
Antes de comparar os frameworks, vamos acompanhar qualquer app SSR desde o momento em que um pull request é mergeado. Clique pelos passos ou aperte o Play.
2. O que cada framework adiciona
A configuração acima é a mesma para os dois. Escolha um para ver o que ele acrescenta do lado da AWS.
Aqui, o servidor é o Lambda. Falo mais sobre ele no final.
- SSG · SSR · ISR
- Otimização de imagens
- Middleware
- Self-hosting documentado
- Adaptador da comunidade
- 6+ tipos de recurso na AWS
- Máximo de 25 behaviors no CloudFront
- Sem build nativo no Windows
- Pouca coisa para manter na AWS
- Servidor que roda em qualquer host
- Streaming opcional no Lambda
- A AWS não é um alvo listado
- Só receitas da comunidade
- Nenhum ISR nem ferramenta de imagens à vista
- Release candidate (nov/2025)
3. Onde o seu código roda
Esta é a maior diferença entre os dois, e quase tudo o que vem a seguir é consequência dela.
- Server Components por padrão (docs)
- Os segredos ficam no servidor
- Menos JS no navegador
'use client'arrasta todo o grafo de imports- Sem React context em Server Components
- As props precisam ser serializáveis
Atenção: 'use client' marca uma fronteira no grafo de módulos, não em um componente isolado. Se você o colocar em um arquivo grande, tudo o que esse arquivo importa vai para o navegador. Use-o em componentes pequenos, lá nas folhas da árvore.
- Um só modelo mental: é React + router
- Código de servidor explícito:
createServerFn - O handler do servidor nunca vai para o bundle do cliente
- Isomórfico por padrão → os loaders rodam também no cliente
process.envno nível do módulo pode vazar- Server Components: experimental, opt-in
Atenção: a própria documentação é clara: "Route loaders are isomorphic". Eles rodam no cliente também, então um segredo ou uma chamada ao banco dentro de um loader é um bug esperando para acontecer. Tudo o que for sensível deve ficar atrás de createServerFn ou createServerOnlyFn.
4. Como o conteúdo fica atualizado
Esta parte define quanta infraestrutura você roda e quanto trabalho de manutenção ela dá.
- Por tempo: stale-while-revalidate
- Sob demanda: tags,
revalidatePath - Atualiza o conteúdo sem redeploy
- O cache e o estado das tags precisam ser compartilhados entre as instâncias
- HTML e RSC precisam ser cacheados juntos
- Mais peças da AWS para monitorar
Há mais detalhes na documentação sobre como a revalidação funciona e na lista de recursos do SST. Um ponto de atenção: se uma CDN cacheia o HTML e o payload RSC com TTLs diferentes, o usuário pode ver conteúdo inconsistente ao navegar no cliente.
Desenhei o pipeline acima numa ordem simplificada.
Dos pontos acima, o descompasso entre HTML e RSC é o que eu esperaria que desse problema primeiro.
- O prerender gera arquivos estáticos comuns
- O cache mais simples possível: S3 + CloudFront
- Os dados de navegação vêm do cache de loaders do router
- Sem ISR na documentação de prerender
- Rotas dinâmicas só são pré-renderizadas se houver link para elas +
crawlLinks - Conteúdo novo = rebuild ou busca em runtime
Se o seu catálogo muda de hora em hora, no Start isso significa rebuilds agendados ou renderização ao vivo. Para muitos apps, tudo bem. Mas se a equipe de conteúdo espera publicar e ver no ar em segundos, compensa pagar pela estrutura do Next/OpenNext.
5. Estratégia de renderização por rota
Uma rota ser estática ou dinâmica depende das APIs que ela usa (docs). Você não declara isso: o Next descobre sozinho a partir do código.
O Next deduz isso pelo que o seu código toca. Dá menos trabalho, mas também mais surpresa: uma chamada aparentemente inofensiva a cookies() pode tornar a página dinâmica sem avisar.
ssr: trueloader no servidorcomponente → HTMLhidrata no cliente'data-only'loader no servidorcomponente só no clientessr: falseloader no clientecomponente no clienteVocê declara isso rota por rota, com o SSR seletivo. Uma rota filha só pode ser mais restrita que a pai, nunca mais permissiva.
O Start pede que você declare isso em cada rota. Dá mais trabalho, mas nada muda escondido por baixo dos panos.
6. Core Web Vitals
O Google avalia a experiência da página com três números. Uma página é considerada "boa" quando atinge a meta em 75% das visitas (web.dev).
O que o framework não decide
Um primeiro byte lento (TTFB) prejudica o LCP, mas o TTFB não é um dos três vitals e, em grande parte, tem pouco a ver com o framework. Ele depende da rapidez da sua API e do banco de dados, da distância entre o servidor e o usuário e de o CloudFront conseguir ou não cachear a resposta (web.dev). Uma página renderizada no servidor pode até ter um TTFB maior que uma renderizada no cliente e mesmo assim ganhar no LCP. Então não escolha um framework por causa do TTFB. Comece corrigindo a chamada de API mais lenta.
Onde os frameworks de fato diferem
| Item | Next.js | TanStack Start | Quem leva |
|---|---|---|---|
| LCP · imagens | Otimização de imagens embutida (com um Lambda próprio na AWS) | <img> comum, seguindo o checklist abaixo, servido do S3 pelo CloudFront | Empate se você seguir o checklist; o Next automatiza o dimensionamento |
| LCP · HTML em cache | Páginas totalmente estáticas são public, então o CloudFront consegue cacheá-las, e o ISR as mantém atualizadas. Páginas dinâmicas são private, no-store e sempre chegam ao servidor (docs) | Páginas pré-renderizadas são arquivos comuns, também cacheáveis (docs). Sem ISR, conteúdo novo exige rebuild | Empate nas estáticas |
| LCP · streaming | Faz streaming com Suspense | SSR com streaming (InfoQ) | Empate, desde que nada no caminho faça buffer |
| INP · JS no navegador | Server Components não enviam JS próprio; só as partes com 'use client' são hidratadas (docs) | Os componentes são renderizados no servidor e hidratados por padrão (docs) | Next, em páginas com muito conteúdo |
| CLS | Depende sobretudo do seu markup: tamanho das imagens, fontes, banners que carregam tarde | Igual | Empate |
Uma boa imagem de LCP no TanStack Start
Estas práticas vêm do guia de LCP do web.dev e valem para qualquer framework:
- Coloque a imagem no HTML renderizado no servidor
fetchpriority="high"só na imagem principal (hero)- Nunca use
loading="lazy"nela srcsetpara o tamanho certo- AVIF ou WebP
// routes/index.tsx
export const Route = createFileRoute('/')({
head: () => ({
links: [
{ rel: 'preload', as: 'image', href: '/img/hero-1200.avif', fetchPriority: 'high' },
],
}),
component: Home,
})
const Home = () => (
<img
src="/img/hero-1200.avif"
srcSet="/img/hero-600.avif 600w, /img/hero-1200.avif 1200w"
sizes="100vw"
width={1200}
height={630}
fetchPriority="high"
alt="Football pitch at sunset"
/>
)No Start, você adiciona tags ao head da página pela opção head da rota (docs). A documentação mostra um preload de fonte, então a versão com imagem acima é uma adaptação minha. Os arquivos ficam no S3 e o CloudFront os cacheia como qualquer outro asset. Como o <img> já está no HTML renderizado no servidor, o preload é opcional; ele faz mais diferença quando a imagem é referenciada a partir de CSS ou JavaScript.
Um teste público mediu 116 KB de JavaScript no cliente para o TanStack Start contra 193 KB para o Next.js, no mesmo app de dashboard (LogRocket). É um único app, então leia como "o runtime do framework pode pesar", e não como "um sempre ganha". A própria página de comparação do TanStack se recusa a apontar um vencedor em tamanho de runtime.
Não encontrei nenhuma comparação medida de Core Web Vitals entre os dois. Rode o Lighthouse e olhe os dados de campo (CrUX) no seu próprio app.
Um site de conteúdo, com poucas partes interativas, aproveita mais os Server Components e o pipeline de imagens do Next. Um app muito interativo envia a maior parte do JS em qualquer um dos dois, então a diferença diminui.
7. O que fica com você quando algo quebra
- O adaptador precisa acompanhar os lançamentos do Next
- Os mantenedores têm capacidade limitada (OpenNext)
- 25 behaviors por distribuição do CloudFront
- Memória e cold starts do Lambda de imagens
- Bugs de consistência de cache e de tags
A superfície de falha é grande, mas muita gente roda essa combinação, então a maioria dos erros dá para achar numa busca. Fixe as versões do Next e do OpenNext juntas e atualize as duas de uma vez.
- Release candidate em nov/2025 (InfoQ)
- Plugin do Nitro para o Vite "under active development"
- Sem runbook oficial para a AWS
- Você mesmo constrói as camadas de imagem e de cache
A superfície de falha é menor, mas menos gente já passou por ali. Quando algo quebrar, você vai ler mais o código do Nitro e do Start do que respostas no Stack Overflow. Conte com isso e confira o status atual do release antes de decidir.
8. Indo mais fundo: Lambda e mutations
Esta parte é opcional. Você pode pulá-la e ainda assim tomar a decisão.
Como uma mutation chega ao Lambda
Posts de formulário e server functions viram uma chamada HTTPS que passa pelo CloudFront até a sua função. Aqui, cada framework tende a falhar de um jeito diferente.
- Lambda = várias instâncias
- Defina
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYou você vai tomar um "Failed to find Server Action" - Em deploys graduais, defina
deploymentId
Fonte: o guia de self-hosting, que descreve isso para configurações com vários servidores.
O Lambda roda várias instâncias, então estou aplicando essa orientação a ele. A documentação não diz isso especificamente sobre o Lambda.
- O build troca o handler por um stub RPC
- O validador (ex.: Zod) é parte central do design
- Middleware de CSRF por padrão
- Só same-origin: APIs públicas precisam de server routes
- Ao definir
src/start.ts, você precisa recolocar o CSRF - A autenticação fica em cada handler
Do guia de server functions: beforeLoad "is not the data boundary".
Trate toda server function como um endpoint público e faça a autorização dentro dela. O mesmo vale para as Server Actions do Next.
Armadilhas que valem para os dois
- Requisições POST pelo OAC exigem
x-amz-content-sha256 - Nada no caminho pode fazer buffer de respostas em streaming
- Com muitas instâncias do Lambda, o Next precisa de cache compartilhado e da mesma chave de criptografia
Veredito
Escolha o Next.js se o conteúdo precisa ir ao ar em segundos, se você quer imagens e middleware resolvidos para você e se aceita ser responsável pelas peças do OpenNext. Escolha o TanStack Start se você quer o menor número de peças na AWS, uma separação explícita entre servidor e cliente e portabilidade, e consegue construir os extras por conta própria.
Faça um protótipo da sua rota mais difícil nos dois antes de decidir e meça os Core Web Vitals no seu próprio app.
Fontes
- Next.js: Self-hosting
- Next.js: Server and Client Components
- Next.js: How revalidation works
- OpenNext for AWS
- SST: Nextjs component (AWS resources, limits)
- TanStack Start: Hosting
- TanStack Start: Execution model
- TanStack Start: Server functions
- TanStack Start: Selective SSR
- TanStack Start: Static prerendering
- TanStack: Start vs Next.js
- Nitro: AWS Lambda preset
- AWS: Restrict access to a Lambda function URL origin
- web.dev: Core Web Vitals
- web.dev: TTFB
- InfoQ: TanStack Start release candidate (Nov 2025)
- LogRocket: TanStack Start RSC vs Next.js RSC