Next.js vs TanStack Start direto na AWS

· 13 min de leitura AWSNext.jsTanStack StartSSR

S3, CloudFront, Route 53 e Lambda, sem Vercel

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.

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. 1. Do merge do PR até a página no navegador
  2. 2. O que cada framework adiciona
  3. 3. Onde o seu código roda
  4. 4. Como o conteúdo fica atualizado
  5. 5. Estratégia de renderização por rota
  6. 6. Core Web Vitals
  7. 7. O que fica com você quando algo quebra
  8. 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.

Deploy · uma vez por mergeRequisição · a cada visitaRepo GitPR → mainPipeline de CIbuild + deployNavegadorRoute 53seu domínioCloudFrontcache de bordaBucket S3arquivos estáticosServidorrenderiza o HTML

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.

NavegadorRoute 53alias → CFACM (us-east-1)CloudFrontS3 · OACassets com hashLambda URL · OACservidor SSRExtras do OpenNextLambda de imagensDynamoDB + SQS (ISR)Warmer · CF FunctionsNitro aws_lambdanão precisa de mais nada

Aqui, o servidor é o Lambda. Falo mais sobre ele no final.

Next.js · via OpenNext · recursos segundo o SST

  • 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

TanStack Start · via Nitro · guia de hospedagem

  • 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.

JS no navegadorLayoutPáginaHeaderGráficoBotãotracejado = Server Component (só HTML + payload)Ilha do botãoRuntime do React+ payload RSC (dados)LayoutPágina + loaderHeaderGráficoBotãosólido = isomórfico: SSR, depois hidratadocreateServerFn()Os 5 componentescódigo do loader (roda nos dois)stub RPC, não o handler
  • 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.env no 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á.

revalidateTag()sob demanda / TTLDynamoDBcache de tagsSQSfila de revalidaçãoFn de revalidaçãorenderiza de novoCache (S3)HTML + RSCa cópia antiga continua sendo servida até a nova ficar prontagit pushconteúdo mudouBuild + prerenderarquivos HTML estáticosUpload para o S3novos assets com hashInvalidação CFlimpa os caminhosou buscar os dados ao vivo nos loaders e dispensar o prerender
  • 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

EstáticaHTML pré-renderizadoO CloudFront serveCache-Control: public
DinâmicaO Lambda renderiza a cada requisiçãousa cookies / headersprivate, no-store
StreamingPrimeiro o shellChunks do Suspense à medida que ficam prontosprecisa de um caminho sem buffer

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 cliente
ssr: falseloader no clientecomponente no cliente

Você 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).

LCP≤ 2,5 sconteúdo principal visível
INP≤ 200 msreage a toques e cliques
CLS≤ 0,1nada pula de lugar

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

ItemNext.jsTanStack StartQuem leva
LCP · imagensOtimização de imagens embutida (com um Lambda próprio na AWS)<img> comum, seguindo o checklist abaixo, servido do S3 pelo CloudFrontEmpate se você seguir o checklist; o Next automatiza o dimensionamento
LCP · HTML em cachePá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 rebuildEmpate nas estáticas
LCP · streamingFaz streaming com SuspenseSSR com streaming (InfoQ)Empate, desde que nada no caminho faça buffer
INP · JS no navegadorServer 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
CLSDepende sobretudo do seu markup: tamanho das imagens, fontes, banners que carregam tardeIgualEmpate

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
  • srcset para 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.

NavegadorCloudFrontPOSTO OAC exige x-amz-content-sha256(nos dois frameworks)Instância Acriptografa variáveis de closureInstância Bprecisa compartilhar a chavehandler do createServerFnvalidador → handlerchecagem de same-origin / CSRF
  • Lambda = várias instâncias
  • Defina NEXT_SERVER_ACTIONS_ENCRYPTION_KEY ou 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

← Todos os artigos