Por que este blog sobre Next.js roda em Astro
A escolha não foi ideológica: um site de conteúdo tem exigências diferentes das de um aplicativo. Os números deste site e onde o Next.js ganharia.
Ambiente testado
- Astro 7.2
- Tailwind CSS 4.3
- Cloudflare Pages
- Node 22
Neste artigo (12)
- A pergunta que apareceu primeiro
- O que um site de conteúdo precisa
- Os números deste site
- O que a escolha entregou na prática
- Realce de código sem custo no navegador
- O build recusa metadado ruim
- Fontes com métricas de fallback
- Cards de compartilhamento gerados no build
- Onde o Next.js ganharia
- O que a escolha custou
- Como confirmar por conta própria
- Se você for fazer o mesmo
A pergunta que apareceu primeiro
A maior parte do que eu escrevo aqui trata de Next.js. Seria coerente que o site fosse feito em Next.js.
Não é. E a razão não é preferência — é que um blog e um aplicativo têm exigências diferentes o suficiente para justificar ferramentas diferentes.
O que um site de conteúdo precisa
Listando sem rodeio o que este site faz:
- entrega texto e blocos de código;
- precisa ser lido por um rastreador que não executa JavaScript com boa vontade;
- tem exatamente três elementos interativos — alternar tema, abrir o menu no celular e destacar a seção atual no índice lateral;
- não tem login, formulário, carrinho nem estado de sessão.
Nenhum framework de aplicação é ruim nisso. Só que todos carregam um runtime para resolver problemas que este site não tem.
Os números deste site
Route
├─ /
├─ /artigos
├─ /artigos/[slug]
├─ /categoria/[cat]
└─ /og/[slug].png gerado em build
JavaScript externo ....... 0 arquivo, 0 KB
JavaScript inline ........ 3,1 KB (tema + índice do artigo)
CSS ...................... 29,2 KB (6791 bytes comprimido)
Fontes ................... 5 arquivos, 120 KB, self-hospedadas
HTML de um artigo ........ 61 KB (11,8 KB comprimido)Zero arquivo de JavaScript. Não é otimização agressiva: é o padrão do Astro. Ele renderiza componentes em tempo de build e envia HTML. Interatividade é opcional, declarada componente a componente.
Os 3,1 KB inline são três coisas específicas: o script que resolve o tema antes
da primeira pintura, o botão que alterna tema, e o IntersectionObserver que
marca a seção atual no índice. Escrevi cada um deles de propósito.
Uma ressalva honesta: esses números são do código do site. O script de anúncios, quando ativo, é de terceiro, pesa muito mais que tudo acima somado e disputa a thread principal durante o carregamento. É justamente por saber disso que o resto é enxuto — o orçamento que sobra vai para o que não está sob meu controle.
O que a escolha entregou na prática
Realce de código sem custo no navegador
Os blocos de código deste artigo passaram pelo Shiki durante o build. O que chega ao navegador é HTML com cores embutidas — sem biblioteca de realce, sem trabalho na thread principal, e funcionando com JavaScript desligado.
O tema duplo é resolvido em CSS:
/* O Shiki grava as duas cores em variáveis no próprio HTML.
A troca de tema é uma regra de CSS, não uma re-renderização. */
[data-theme='dark'] .astro-code,
[data-theme='dark'] .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}O build recusa metadado ruim
Os artigos são arquivos MDX com frontmatter validado por um esquema. Isso não é enfeite: os dois campos que o Google exibe no resultado de busca têm limite, e estourar o limite custa clique.
schema: z.object({
// Acima de 70 caracteres o título é truncado no resultado de busca.
title: z.string().min(15).max(70),
// Faixa em que a descrição costuma ser exibida inteira.
description: z.string().min(110).max(165),
category: z.enum(['dados', 'apis', 'performance']),
stack: z.array(z.string()).min(1),
errorMessage: z.string().optional(),
draft: z.boolean().default(true),
})Um título longo demais quebra o build. É uma regra de SEO transformada em erro de compilação, que é o único lugar onde regra de SEO não é esquecida.
O draft: true por padrão significa que nenhum artigo vai ao ar por acidente —
publicar exige uma edição explícita.
Fontes com métricas de fallback
O Astro 7 baixa a fonte durante o build, serve do próprio domínio e gera automaticamente uma fonte de fallback ajustada:
@font-face {
font-family: 'IBM Plex Sans fallback: Arial';
src: local('Arial');
size-adjust: 99.5961%;
ascent-override: 102.9157%;
descent-override: 27.6115%;
}
Esses números fazem a fonte de sistema ocupar exatamente o mesmo espaço que a fonte real. Quando a troca acontece, nada se move — o texto aparece de imediato e o CLS fica em zero. Escrever isso à mão é possível e chato; ganhar de graça é melhor.
Cards de compartilhamento gerados no build
Um por artigo, com título, assunto, a mensagem de erro e a stack. Vinte e quatro artigos são vinte e quatro imagens que ninguém precisou desenhar.
Onde o Next.js ganharia
A comparação honesta precisa dos dois lados.
| Necessidade | Ferramenta que vence |
|---|---|
| Conteúdo majoritariamente estático | Astro |
| Login, sessão, dados por usuário | Next.js |
| Formulários com mutação e revalidação | Next.js |
| Painel com muita interação | Next.js |
| Ilhas pequenas de interatividade em páginas de conteúdo | Astro |
| Ecossistema de bibliotecas React | Next.js |
| Enviar o mínimo possível de JavaScript | Astro |
Se este site tivesse área de assinante, comentários próprios ou qualquer coisa com estado por usuário, a conta mudaria. Manter duas arquiteturas para juntar blog e aplicativo costuma custar mais do que aceitar um pouco de JavaScript extra nas páginas de conteúdo.
O que a escolha custou
Componentes React não vêm de graça. Dá para usar — o Astro suporta React, Vue, Svelte e outros — mas cada componente interativo carrega o runtime do framework dele. Um único componente React numa página tira o site do zero absoluto. Aqui, os três elementos interativos foram escritos sem framework justamente por isso.
Menos material de referência. Para cada problema de Astro existem dez respostas de Next.js. Quando aparece algo específico, você lê o código-fonte.
O Astro 7 mudou o processamento de Markdown. Ele trocou o remark/rehype por um processador nativo em Rust. É mais rápido e é estrito com HTML inválido — uma tag não fechada agora é erro de build. O efeito prático: praticamente todo tutorial de plugin de Markdown para Astro que você encontrar está desatualizado, e a adaptação é por conta própria.
Como confirmar por conta própria
Desligue o JavaScript. No DevTools, Command Palette → Disable JavaScript, e recarregue. Todo o conteúdo continua legível, os links funcionam, o realce de código permanece. O que se perde é o botão de tema e o destaque do índice — que é exatamente o esperado de um site cuja interatividade é opcional.
Veja o código-fonte. Ctrl+U em qualquer artigo mostra o texto inteiro no
HTML. É assim que o rastreador vê a página, sem depender de execução de script.
Conte as requisições. Na aba Network, filtrando por domínio próprio, um
artigo carrega o documento, um CSS e as fontes. Nenhum arquivo .js meu.
Se você for fazer o mesmo
Três decisões que renderam mais que as outras, para quem estiver montando algo parecido:
Valide o frontmatter com esquema. Regra de SEO que vive na cabeça de alguém é regra que some. Regra que quebra o build fica.
Rascunho como padrão. draft: true no esquema, e publicar exige ato
consciente.
Escreva as poucas interações à mão. Três componentes interativos não justificam um framework de interface. Foram cerca de sessenta linhas de JavaScript no total, e elas são a diferença entre 3 KB e 90 KB.
Continue por aqui
Performance Web
LCP travado por fonte: preload, font-display e métricas de fallback
Quando o maior elemento da tela é texto, o LCP espera a fonte chegar. Como fazer o texto pintar de imediato sem trocar LCP por deslocamento de layout.
Performance Web
INP acima de 200 ms: achando a long task que trava o clique
O INP mede o clique mais lento da sessão, não a média. Como achar exatamente qual interação e qual linha de código estão travando a thread principal.
Performance Web
Web Push do zero: Service Worker, VAPID e o pedido de permissão
A implementação inteira, do registro do worker ao envio. E o erro de UX que queima a permissão do usuário para sempre, sem possibilidade de desfazer.