Server Components: o que muda quando o componente não vai para o browser
Os exemplos deste artigo são todos deste site — código que está a correr enquanto o lê, e não casos inventados para a ocasião.
A ideia é uma só: no App Router do Next, um componente corre no servidor por omissão e nunca chega ao browser. Não é uma afinação de desempenho; muda o que se pode escrever dentro de um componente, e muda onde é que as coisas passam a doer.
1. O que se ganha, em bytes
O realce de sintaxe dos blocos de código deste blog usa o highlight.js com doze gramáticas registadas. Tudo isso corre no servidor. O que chega ao browser é HTML com span já pintadas — zero bytes de biblioteca de realce.
Numa aplicação só de cliente, aquelas doze gramáticas teriam de ser descarregadas por cada visitante, para colorir um texto que já estava escrito.
2. Dados sem API pelo meio
A coluna da direita deste site é um componente assíncrono que fala com a base de dados:
// Sem 'use client': corre no servidor, e só lá
export async function ColunaInfo() {
const [frase, sondagem] = await Promise.all([
lerFraseAleatoria(),
lerSondagem(),
]);
return <aside>...</aside>;
}Não há fetch, não há rota de API, não há estado de carregamento. A consulta corre onde a base está, e o componente já nasce com o resultado. Aquele Promise.all também não é enfeite: são duas consultas independentes, e em série seriam duas idas à base uma atrás da outra.
3. O 'use client' é uma fronteira, não um ficheiro
Esta é a parte que custa a interiorizar, e a que dá os problemas reais. Marcar um ficheiro com 'use client' não manda só aquele ficheiro para o browser: manda também tudo o que ele importa, e tudo o que esses importarem.
Neste site há um ficheiro que só existe por causa disso. A barra de botões do editor precisa da lista de linguagens para mostrar um menu. Se a lista vivesse ao lado do realce, ficava assim:
// realce.ts — servidor
import hljs from 'highlight.js/lib/core';
import bash from 'highlight.js/lib/languages/bash';
// ... mais onze gramáticas
export const LINGUAGENS = [ ... ];// editor.tsx — cliente
'use client';
import { LINGUAGENS } from '@/lib/realce'; // e o highlight.js vai atrásDez nomes num menu passavam a arrastar a biblioteca inteira para o browser de quem escreve. A solução tem uma linha de código e zero de esperteza: a lista mudou-se para um ficheiro sem dependências nenhumas.
// linguagens.ts — não importa nada, atravessa sem peso
export const LINGUAGENS_DISPONIVEIS = [
{ valor: 'typescript', etiqueta: 'TypeScript' },
{ valor: 'bash', etiqueta: 'Shell' },
];Um teste garante que cada entrada desta lista corresponde a uma gramática registada do outro lado. Separar dois ficheiros que têm de andar a par pede uma rede por baixo.
4. O que atravessa a fronteira tem de ser dados
As props que vão de um componente de servidor para um de cliente são serializadas. Números, texto, listas e objetos simples passam. Funções não passam.
// Não atravessa: uma função não se serializa
<TabelaAdmin colunas={[{ chave: 'nome', render: (v) => <b>{v}</b> }]} />
// Atravessa: dados que descrevem o que fazer
<TabelaAdmin colunas={[{ chave: 'nome', etiqueta: 'Nome' }]} />A tabela da administração deste site é exatamente isto: as colunas são declarativas porque o componente é de cliente, e quando uma célula precisa de tratamento — resumir HTML, formatar uma data — isso é feito no servidor, e chega aqui já como texto.
5. Ponha o 'use client' nas folhas
Este site tem doze ficheiros marcados como cliente, e são todos pequenos: o alternador de tema, o menu do telemóvel, o formulário de comentários, a sondagem. As páginas em si são todas de servidor.
E um componente de cliente pode receber componentes de servidor como children:
<Acordeao> {/* cliente: guarda o aberto/fechado */}
<ListaArtigos /> {/* servidor: desenhado no servidor e entregue já feito */}
</Acordeao>O Acordeao nunca chega a executar a ListaArtigos — recebe-a como conteúdo já produzido. É assim que se põe interatividade à volta de conteúdo pesado sem arrastar esse conteúdo para o browser.
6. server-only: transformar uma fuga num erro de build
// src/server/db.ts
import 'server-only';Oito ficheiros deste site começam assim. Aquela linha não faz nada em execução: rebenta o build se alguém, um dia, importar este módulo a partir de um ficheiro de cliente.
Sem ela, o erro é silencioso e do pior tipo — a chave da base de dados, ou o segredo de autenticação, a serem incluídos no JavaScript que qualquer pessoa lê com o botão direito.
7. As Server Actions, e a armadilha que trazem
A viagem no sentido contrário: um 'use server' deixa o cliente chamar uma função que corre no servidor.
'use server';
export const guardarArtigo = comSessao(async (dados) => {
// ...
});Aquele comSessao não é estilo. Uma Server Action é um endpoint público, invocável por quem souber o identificador dela. O proxy deste site protege as páginas de /admin; não defende as ações. Se uma delas não verificar a sessão por si, está aberta a quem a encontrar.
Como é o género de coisa que regride ao acrescentar a ação seguinte — e não ao mexer nas que já lá estão —, há um teste que lê o ficheiro das ações e falha se alguma exportada escapar ao invólucro.
8. O que se perde
Num componente de servidor não há useState, não há useEffect, não há onClick. Nada disso faz sentido onde não há browser. Uma biblioteca de terceiros que use hooks obriga a 'use client' — e, com ela, tudo o que ela importa.
É a razão pela qual vale a pena decidir isto cedo. Um 'use client' no sítio errado, lá em cima na árvore, torna cliente metade da aplicação sem ninguém dar por isso.
O resumo
Servidor por omissão; cliente onde houver estado ou eventos, e o mais abaixo possível na árvore. O que atravessa são dados. E o que corre no servidor não é automaticamente seguro por correr no servidor: uma Server Action é uma porta para a rua, e tem de ser tratada como tal.