A regra de ouro: o trabalho mais rápido é o que você não faz
Otimizar o desempenho é, quase sempre, evitar trabalho repetido: não recalcular, não pedir de novo, não enviar o que já está lá. A ferramenta central para isso é o cache, e convém pensá-lo em níveis.
Cache por níveis
Navegador / HTTP: o cliente guarda respostas conforme os cabeçalhos
Cache-ControleETag. Se o recurso não mudou, o servidor responde304 Not Modifiede não reenvia o corpo. Custo de rede: zero.CDN para estáticos: uma rede de servidores próximos ao usuário serve imagens, JS e CSS a partir da borda, sem tocar no seu origin. Mais perto = menos latência e menos carga no seu backend.
Cache no backend (Redis) com o padrão cache-aside:
está no cache? → sim → devolve (rápido) → não → vai ao BD, guarda o resultado no cache, devolveA aplicação gerencia o cache "ao lado" do BD. É preciso decidir o TTL (quanto tempo o dado vive) e invalidar a entrada quando o dado muda, ou você servirá dados velhos. Invalidar um cache é um dos problemas verdadeiramente difíceis.
No frontend: enviar menos e mais tarde
- Lazy loading: carregue as coisas quando forem necessárias, não ao iniciar (imagens ao rolar, uma rota ao visitá-la).
- Code splitting: parta o bundle em pedaços e baixe só o que a tela atual precisa. O usuário não paga por código que ainda não usa.
- Compressão: o servidor comprime as respostas de texto (HTML, JS, CSS, JSON) com gzip ou brotli (melhor taxa) antes de enviá-las. Um JS de 300 KB pode viajar como 80 KB.
No banco de dados: índices e o problema N+1
O BD costuma ser o gargalo. Dois clássicos:
- Índices: sem índice, buscar uma linha implica percorrer a tabela inteira
(full scan). Um índice sobre a coluna pela qual você filtra
(
WHERE email = ?) transforma esse percurso numa busca quase instantânea. Custam algo em escrita e espaço, então se indexam as colunas pelas quais se filtra e ordena, não todas. - O problema N+1: você pede uma lista de N elementos (1 consulta) e depois,
para cada um, faz outra consulta para seus dados relacionados. Resultado:
1 + N consultas. Com 100 pedidos são 101 viagens ao BD. A solução é
agrupar (batching): carregar os dados dos N numa única consulta
(
WHERE id IN (...)) ou com umJOIN.
Meça antes de otimizar. "Acho que isto é lento" não é um dado; um p95 e um trace, sim. Otimizar às cegas adiciona complexidade sem garantia de melhora.
Exemplos
Cache-aside com um Map: não recalcular o já calculado
function cachear(fn) {
const cache = new Map();
return function (chave) {
if (cache.has(chave)) return cache.get(chave);
const valor = fn(chave);
cache.set(chave, valor);
return valor;
};
}
let chamadas = 0;
const quadrado = cachear((n) => { chamadas++; return n * n; });
quadrado(8); quadrado(8); quadrado(8);
console.log("resultado:", quadrado(8), "chamadas reais:", chamadas); // 64, 1
N+1 vs. batching: 1 consulta em vez de N
// Simula uma carga "em bloco" de vários ids de uma vez.
function carregarUsuarios(ids) {
console.log("consultas ao BD:", 1, "para", ids.length, "ids");
return ids.map((id) => ({ id, nome: "Usuário " + id }));
}
const ids = [1, 2, 3, 4, 5];
const usuarios = carregarUsuarios(ids); // 1 viagem, não 5
console.log(usuarios.length, "usuários carregados");