Uma aplicação lenta é uma aplicação quebrada. Estudos da Google mostram que cada 100ms de atraso no carregamento reduz conversões em 1%. Após 3 segundos de espera, mais da metade dos usuários abandona a página. Performance não é um detalhe estético — é uma funcionalidade central.
O problema com otimização é que é fácil otimizar a coisa errada. Desenvolvedores frequentemente passam horas ajustando detalhes que impactam milissegundos enquanto ignoram gargalos que custam segundos. Por isso, a regra número um de performance é: meça primeiro, otimize depois.
Este artigo ensina como medir corretamente, onde os gargalos mais comuns aparecem, e as técnicas mais eficazes para eliminá-los — tanto no front-end React quanto no back-end Node.js.
Medindo performance — as métricas que importam
Antes de otimizar qualquer coisa, precisamos estabelecer o que estamos medindo. O Google definiu as Core Web Vitals como as métricas fundamentais de experiência do usuário.
LCP (Largest Contentful Paint) mede quanto tempo leva para o maior elemento visível da página ser renderizado. Representa quando o usuário percebe que a página "carregou". A meta é abaixo de 2,5 segundos.
FID (First Input Delay) ou seu sucessor INP (Interaction to Next Paint) mede quanto tempo o navegador leva para responder à primeira interação do usuário — um clique, um toque. A meta é abaixo de 100ms.
CLS (Cumulative Layout Shift) mede a estabilidade visual — quanto os elementos da página se movem enquanto carregam. Nada mais frustrante do que clicar em um botão que se moveu. A meta é abaixo de 0,1.
// Medindo Core Web Vitals no React com a biblioteca oficial
// npm install web-vitals
// src/utils/webVitals.js
import { onCLS, onFID, onLCP, onINP, onTTFB } from 'web-vitals';
// Função que envia as métricas para um serviço de analytics
// Em produção, você enviaria para o Google Analytics, Datadog, etc.
function reportarMetrica(metrica) {
console.log(`[Web Vitals] ${metrica.name}: ${Math.round(metrica.value)}ms`);
// Exemplo de envio para o Google Analytics 4
if (window.gtag) {
window.gtag('event', metrica.name, {
event_category: 'Web Vitals',
event_label: metrica.id,
value: Math.round(
// LCP e TTFB são em ms — CLS é adimensional (multiplica por 1000 para GA)
metrica.name === 'CLS' ? metrica.value * 1000 : metrica.value
),
non_interaction: true, // não conta como bounce no GA
});
}
}
// Registra todos os observers das métricas
export function iniciarMonitoramento() {
onCLS(reportarMetrica); // Cumulative Layout Shift
onFID(reportarMetrica); // First Input Delay (browsers antigos)
onINP(reportarMetrica); // Interaction to Next Paint (atual)
onLCP(reportarMetrica); // Largest Contentful Paint
onTTFB(reportarMetrica); // Time to First Byte (velocidade do servidor)
}
// src/main.jsx — ativa o monitoramento em produção
import { iniciarMonitoramento } from './utils/webVitals';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// Só monitora em produção — em dev causaria ruído desnecessário
if (import.meta.env.PROD) {
iniciarMonitoramento();
}
Ferramentas de medição
Antes de escrever uma linha de otimização, use estas ferramentas para entender onde estão os gargalos reais.
Lighthouse (Google Chrome DevTools)
→ Análise completa: performance, acessibilidade, SEO, boas práticas
→ Abre DevTools → aba Lighthouse → Generate report
→ Teste em modo incógnito (sem extensões interferindo)
→ Simula conexão lenta (3G) para cenários reais
Chrome DevTools — Network
→ Waterfall de carregamento: veja o que está bloqueando
→ Filtre por JS, CSS, Fetch para analisar cada tipo
→ "Disable cache" para simular primeira visita
→ Throttling para simular 3G ou 4G lento
Chrome DevTools — Performance
→ Grava a execução e mostra flame chart
→ Identifica funções lentas e long tasks (>50ms)
→ Mostra quando o main thread está bloqueado
Chrome DevTools — Coverage
→ Mostra qual porcentagem do JS/CSS está sendo usada
→ Código não usado = bundle desnecessariamente grande
PageSpeed Insights (pagespeed.web.dev)
→ Usa dados reais de usuários do Chrome (CrUX data)
→ Distinção entre lab data e field data
→ Grátis e não requer instalação
WebPageTest (webpagetest.org)
→ Testa de locais específicos (São Paulo, por exemplo)
→ Comparação antes/depois de otimizações
→ Relatórios detalhados com filmstrip visual
Performance no front-end React
Bundle size — o problema mais comum
O maior impacto em performance de front-end geralmente vem do tamanho do JavaScript enviado ao navegador. JavaScript precisa ser baixado, parseado e executado — é o recurso mais caro por byte.
// vite.config.js — analisando e otimizando o bundle
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// npm install -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
// Gera stats.html após o build com mapa visual do bundle
// Abra o arquivo para ver quais bibliotecas ocupam mais espaço
visualizer({
open: true, // abre automaticamente no browser após build
gzipSize: true, // mostra tamanho após gzip (mais realista)
brotliSize: true, // e após brotli
}),
],
build: {
rollupOptions: {
output: {
// Code splitting manual — agrupa bibliotecas em chunks lógicos
// Benefício: se react-router não mudar, o browser usa o cache anterior
manualChunks: {
// Bibliotecas que raramente mudam ficam em cache por mais tempo
'vendor-react': ['react', 'react-dom'],
'vendor-router': ['react-router-dom'],
'vendor-query': ['@tanstack/react-query'],
'vendor-store': ['zustand'],
},
},
},
},
});
# Após npm run build, examine o output:
# dist/assets/index-[hash].js → código da aplicação
# dist/assets/vendor-react-[hash].js → react e react-dom
# dist/assets/produtos-[hash].js → página de produtos (lazy)
# Analise tamanhos:
ls -lh dist/assets/*.js
# Verifique tamanhos gzipados (mais realista — servidores comprimem):
gzip -k dist/assets/*.js && ls -lh dist/assets/*.js.gz
Lazy loading e code splitting
Já vimos lazy loading com React Router no Módulo 6. Aqui vamos mais fundo — lazy loading pode ser aplicado a qualquer componente pesado, não apenas a páginas.
// Lazy loading de componente pesado que não aparece imediatamente
// ❌ Importa o editor de texto RICO junto com o bundle principal
// (bibliotecas como TipTap, QuillJS, Monaco têm centenas de KB)
import RichTextEditor from './RichTextEditor';
function FormularioProduto() {
return (
<div>
<input type="text" />
<RichTextEditor /> {/* carregado mesmo em /login */}
</div>
);
}
// ✅ Só carrega o editor quando o componente for renderizado
const RichTextEditor = lazy(() => import('./RichTextEditor'));
function FormularioProduto() {
return (
<div>
<input type="text" />
<Suspense fallback={<div className="editor-skeleton" />}>
<RichTextEditor />
</Suspense>
</div>
);
}
// Lazy loading condicional — só carrega se o usuário é admin
function Dashboard() {
const eAdmin = useAuthStore((s) => s.usuario?.papel === 'admin');
// O PainelAdmin só é importado se eAdmin for true
const PainelAdmin = eAdmin ? lazy(() => import('./PainelAdmin')) : null;
return (
<div>
<ResumoGeral />
{eAdmin && PainelAdmin && (
<Suspense fallback={<p>Carregando painel...</p>}>
<PainelAdmin />
</Suspense>
)}
</div>
);
}
Otimizando re-renders — React.memo, useMemo, useCallback
Re-renders desnecessários são o gargalo de runtime mais comum em aplicações React. O problema é que eles são silenciosos — você não vê na tela, mas o browser está trabalhando à toa.
// Instalando o React DevTools Profiler para identificar re-renders
// 1. Instale a extensão React DevTools no Chrome
// 2. Abra DevTools → aba Profiler
// 3. Clique em "Record" → interaja com a página → pare a gravação
// 4. Veja quais componentes re-renderizaram e por quê
// ── IDENTIFICANDO O PROBLEMA ────────────────────────
function ListaProdutos({ produtos, onRemover }) {
console.count('ListaProdutos renderizou'); // debug temporário
return (
<ul>
{produtos.map((p) => (
// CardProduto re-renderiza toda vez que ListaProdutos re-renderiza
// mesmo que as props do card não tenham mudado
<CardProduto key={p.id} produto={p} onRemover={onRemover} />
))}
</ul>
);
}
// ── SOLUÇÃO 1: React.memo ───────────────────────────
// memo() faz um shallow comparison das props
// Se as props não mudaram (mesma referência), pula a re-renderização
const CardProduto = memo(function CardProduto({ produto, onRemover }) {
console.count(`CardProduto ${produto.id} renderizou`);
return (
<li>
{produto.nome} — R$ {produto.preco}
<button onClick={() => onRemover(produto.id)}>Remover</button>
</li>
);
});
// ── SOLUÇÃO 2: useCallback para estabilizar funções ─
// Sem useCallback, onRemover é uma nova função a cada render
// → memo() percebe que a prop mudou → re-renderiza mesmo assim
function PaginaProdutos() {
const [produtos, setProdutos] = useState([...]);
const [outraCoisa, setOutraCoisa] = useState(0);
// ❌ Nova referência a cada render — memo() não adianta
const handleRemover = (id) => {
setProdutos((prev) => prev.filter((p) => p.id !== id));
};
// ✅ Mesma referência entre renders — memo() funciona
const handleRemover = useCallback((id) => {
setProdutos((prev) => prev.filter((p) => p.id !== id));
}, []); // [] porque usa padrão funcional do setState
return (
<div>
<button onClick={() => setOutraCoisa((n) => n + 1)}>
Clique: {outraCoisa}
{/* Clicar aqui NÃO vai re-renderizar os CartõesProduto */}
</button>
<ListaProdutos produtos={produtos} onRemover={handleRemover} />
</div>
);
}
// ── SOLUÇÃO 3: useMemo para cálculos derivados ──────
function EstatisticasProdutos({ produtos }) {
// ❌ Recalcula em todo render — mesmo quando produtos não mudou
const estatisticas = {
total: produtos.length,
precoMedio: produtos.reduce((s, p) => s + p.preco, 0) / produtos.length,
maisCaros: produtos.filter((p) => p.preco > 1000).length,
semEstoque: produtos.filter((p) => p.estoque === 0).length,
};
// ✅ Só recalcula quando produtos muda
const estatisticasMemo = useMemo(() => ({
total: produtos.length,
precoMedio: produtos.reduce((s, p) => s + p.preco, 0) / produtos.length,
maisCaros: produtos.filter((p) => p.preco > 1000).length,
semEstoque: produtos.filter((p) => p.estoque === 0).length,
}), [produtos]);
return (
<div>
<p>Total: {estatisticasMemo.total}</p>
<p>Preço médio: R$ {estatisticasMemo.precoMedio.toFixed(2)}</p>
</div>
);
}
Otimizando listas longas — virtualização
Renderizar milhares de itens no DOM ao mesmo tempo é lento. A virtualização renderiza apenas os itens visíveis na tela — o resto é "simulado" com espaço vazio.
// npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function ListaVirtualizada({ itens }) {
// Referência ao elemento pai (o container que tem scroll)
const containerRef = useRef(null);
const virtualizador = useVirtualizer({
count: itens.length, // total de itens
getScrollElement: () => containerRef.current, // elemento com scroll
estimateSize: () => 72, // altura estimada de cada item em px
overscan: 5, // renderiza 5 itens extras acima/abaixo da viewport
// (evita flash de conteúdo ao rolar rapidamente)
});
return (
// Container com altura fixa e overflow-y: auto
<div
ref={containerRef}
style={{ height: '600px', overflowY: 'auto' }}
>
{/*
Div interna com altura total calculada pelo virtualizador
Isso cria o espaço de scroll correto sem renderizar todos os itens
*/}
<div style={{ height: `${virtualizador.getTotalSize()}px`, position: 'relative' }}>
{virtualizador.getVirtualItems().map((itemVirtual) => {
const item = itens[itemVirtual.index];
return (
<div
key={itemVirtual.key}
// Posiciona cada item virtualmente no lugar correto
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${itemVirtual.size}px`,
transform: `translateY(${itemVirtual.start}px)`,
}}
>
<ItemProduto produto={item} />
</div>
);
})}
</div>
</div>
);
}
// Com 10.000 itens: sem virtualização → 10.000 nós no DOM
// Com virtualização → ~15 nós no DOM → diferença brutal de performance
Imagens — o maior ofensor de LCP
Imagens mal otimizadas são a causa número um de LCP alto. As técnicas são simples mas impactantes.
// ── LAZY LOADING DE IMAGENS ─────────────────────────
// O atributo loading="lazy" é suportado por todos os browsers modernos
// A imagem só é baixada quando está perto de entrar na viewport
// ❌ Baixa todas as imagens da lista imediatamente
function CardProduto({ produto }) {
return (
<div>
<img src={produto.imagem} alt={produto.nome} />
</div>
);
}
// ✅ Lazy loading nativo — zero JavaScript necessário
function CardProduto({ produto }) {
return (
<div>
<img
src={produto.imagem}
alt={produto.nome}
loading="lazy" // browser decide quando baixar
decoding="async" // decodifica sem bloquear o main thread
width={300} // sempre especifique dimensões
height={200} // evita CLS (layout shift ao carregar)
/>
</div>
);
}
// ── A IMAGEM HERO (LCP) DEVE SER PRIORITÁRIA ────────
// A imagem principal da página (o LCP) NÃO deve ter lazy loading
// Ao contrário — deve ter fetchpriority="high"
function HeroProduto({ produto }) {
return (
<img
src={produto.imagemPrincipal}
alt={produto.nome}
fetchPriority="high" // instrui o browser a baixar primeiro
decoding="async"
width={800}
height={600}
/>
);
}
// ── FORMATOS MODERNOS ────────────────────────────────
// WebP e AVIF são muito menores que JPEG/PNG com mesma qualidade
// Use a tag <picture> para servir o formato certo para cada browser
function ImagemOtimizada({ src, alt, width, height }) {
// Remove a extensão e gera caminhos para cada formato
const base = src.replace(/.(jpg|jpeg|png)$/i, '');
return (
<picture>
{/* Browser tenta AVIF primeiro (menor, mais moderno) */}
<source srcSet={`${base}.avif`} type="image/avif" />
{/* Fallback para WebP (amplo suporte) */}
<source srcSet={`${base}.webp`} type="image/webp" />
{/* Fallback final para JPEG/PNG (todos os browsers) */}
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
</picture>
);
}
Performance no back-end Node.js
Otimizando queries ao MongoDB
O banco de dados é o gargalo mais comum em APIs. Consultas sem índices, dados em excesso e múltiplos round-trips são os culpados mais frequentes.
// ── ÍNDICES — a otimização de maior impacto ──────────
// Uma query sem índice faz full collection scan — lê TODOS os documentos
// Com índice, encontra os documentos diretamente — diferença de 100x ou mais
// Verificando se suas queries usam índices
// No MongoDB Compass ou mongosh:
// db.tarefas.find({ usuario: ObjectId(...) }).explain('executionStats')
// Procure por: "IXSCAN" (usa índice) vs "COLLSCAN" (não usa — problema!)
// No Mongoose, defina índices no schema:
const tarefaSchema = new Schema({
titulo: String,
status: String,
prioridade: String,
usuario: { type: Schema.Types.ObjectId, ref: 'Usuario' },
criadoEm: Date,
});
// Índice composto — otimiza a query mais comum da aplicação:
// Tarefa.find({ usuario: id, status: 'pendente' }).sort({ criadoEm: -1 })
// O índice cobre exatamente este padrão de consulta
tarefaSchema.index({ usuario: 1, status: 1, criadoEm: -1 });
// Índice de texto — otimiza buscas por texto livre
tarefaSchema.index({ titulo: 'text', descricao: 'text' });
// ── LEAN() — consultas somente leitura mais rápidas ─
// Por padrão, o Mongoose transforma cada documento em um objeto com
// métodos, getters, setters e toda a maquinaria do ODM.
// .lean() retorna plain JavaScript objects — muito mais rápido
// ❌ Sem .lean() — cria objetos Mongoose completos (mais memória, mais CPU)
const tarefas = await Tarefa.find({ usuario: id });
// ✅ Com .lean() — retorna objetos JS simples
// Use sempre que não precisar de métodos de instância (save, etc.)
const tarefas = await Tarefa.find({ usuario: id }).lean();
// ── SELECT — busque apenas os campos necessários ─────
// Buscar documentos completos quando você precisa de 3 campos
// desperdiça largura de banda e memória
// ❌ Retorna todos os campos (pode ser centenas de KB por documento)
const tarefas = await Tarefa.find({ usuario: id });
// ✅ Retorna apenas os campos necessários para a listagem
const tarefas = await Tarefa
.find({ usuario: id })
.select('titulo status prioridade criadoEm')
.lean();
// ── PARALELISMO — queries independentes em paralelo ─
// Se duas queries não dependem uma da outra, rode-as juntas
// ❌ Sequencial — a segunda espera a primeira terminar
async function estatisticasDashboard(usuarioId) {
const totalTarefas = await Tarefa.countDocuments({ usuario: usuarioId });
const totalProdutos = await Produto.countDocuments({ ativo: true });
// tempo total = tempo(tarefas) + tempo(produtos)
return { totalTarefas, totalProdutos };
}
// ✅ Paralelo — ambas rodam ao mesmo tempo
async function estatisticasDashboard(usuarioId) {
// Promise.all executa ambas simultaneamente
const [totalTarefas, totalProdutos] = await Promise.all([
Tarefa.countDocuments({ usuario: usuarioId }),
Produto.countDocuments({ ativo: true }),
]);
// tempo total = max(tempo(tarefas), tempo(produtos))
return { totalTarefas, totalProdutos };
}
// ── PAGINAÇÃO — nunca busque tudo de uma vez ─────────
// ❌ Retorna TODOS os documentos — perigoso com grandes coleções
const todasAsTarefas = await Tarefa.find({ usuario: id });
// ✅ Paginação com skip/limit
const pagina = Number(req.query.pagina) || 1;
const porPagina = Math.min(Number(req.query.por_pagina) || 10, 50);
const skip = (pagina - 1) * porPagina;
const [tarefas, total] = await Promise.all([
Tarefa.find({ usuario: id })
.sort({ criadoEm: -1 })
.skip(skip)
.limit(porPagina)
.lean(),
Tarefa.countDocuments({ usuario: id }),
]);
Cache — evitando trabalho repetido
Cache é a otimização de maior retorno para dados que não mudam a cada requisição. A ideia é simples: calcule uma vez, sirva muitas vezes.
// Cache em memória com node-cache (para dados de curta duração)
// npm install node-cache
const NodeCache = require('node-cache');
// TTL de 5 minutos — dados expiram e são recalculados automaticamente
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 });
async function buscarEstatisticasComCache(usuarioId) {
const chave = `estatisticas:${usuarioId}`;
// Tenta o cache primeiro — O(1), instantâneo
const emCache = cache.get(chave);
if (emCache) {
console.log('[Cache] HIT:', chave);
return emCache;
}
// Cache miss — calcula do zero (aggregation custosa)
console.log('[Cache] MISS:', chave);
const dados = await Tarefa.aggregate([
{ $match: { usuario: mongoose.Types.ObjectId(usuarioId) } },
{
$group: {
_id: '$status',
total: { $sum: 1 },
},
},
]);
// Salva no cache para próximas requisições
cache.set(chave, dados);
return dados;
}
// Invalidação do cache quando os dados mudam
async function criarTarefa(usuarioId, dados) {
const tarefa = await Tarefa.create({ ...dados, usuario: usuarioId });
// Remove o cache do usuário — será recalculado na próxima requisição
cache.del(`estatisticas:${usuarioId}`);
return tarefa;
}
Compressão — reduzindo tráfego de rede
Compressão gzip ou brotli reduz o tamanho das respostas HTTP em 60-80% para texto (JSON, HTML, CSS). É uma das otimizações mais fáceis de implementar.
// npm install compression
const compression = require('compression');
app.use(
compression({
// Só comprime respostas maiores que 1KB
// Respostas pequenas não se beneficiam da compressão
threshold: 1024,
// Nível de compressão: 1 (rápido, menos compressão) a 9 (lento, mais compressão)
// 6 é o padrão — bom equilíbrio entre velocidade e compressão
level: 6,
// Não comprime streams de vídeo ou imagens (já são binários comprimidos)
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
})
);
// A compressão é transparente — o cliente recebe dados menores,
// descomprime automaticamente. Você não muda nada no código das rotas.
Monitorando performance em produção
Saber que sua aplicação ficou lenta após um deploy é inestimável. O Node.js tem APIs nativas para medir performance.
// src/middlewares/performance.js
// Middleware que mede o tempo de cada requisição e loga as lentas
function monitorarPerformance(req, res, next) {
// performance.now() tem precisão de submilissegundo
const inicio = performance.now();
// Intercepta o momento em que a resposta é finalizada
res.on('finish', () => {
const duracaoMs = performance.now() - inicio;
// Loga apenas requisições lentas (> 500ms) para não poluir os logs
if (duracaoMs > 500) {
console.warn(
`[Slow Request] ${req.method} ${req.path} — ${duracaoMs.toFixed(2)}ms`
);
}
// Adiciona header de timing para debugging no DevTools
// Visível em DevTools → Network → Timing
if (process.env.NODE_ENV === 'development') {
res.setHeader('Server-Timing', `total;dur=${duracaoMs.toFixed(2)}`);
}
});
next();
}
module.exports = { monitorarPerformance };
// Profiling de funções críticas com console.time
// Use durante desenvolvimento para medir operações específicas
async function listarComFiltros(filtros) {
console.time('listarComFiltros:query');
const resultado = await Produto
.find(filtros)
.sort('-criadoEm')
.limit(10)
.lean();
console.timeEnd('listarComFiltros:query');
// Output: listarComFiltros:query: 45.234ms
return resultado;
}
Checklist de performance
Front-end
─────────────────────────────────────────────────────────
[ ] Lighthouse score > 90 em Performance
[ ] Lazy loading em todas as páginas (React.lazy + Suspense)
[ ] Code splitting manual para bibliotecas grandes (manualChunks)
[ ] Imagens com loading="lazy" (exceto hero/LCP)
[ ] Imagem LCP com fetchPriority="high"
[ ] Dimensões explícitas em todas as imagens (evita CLS)
[ ] React.memo em componentes de lista que recebem callbacks
[ ] useMemo para cálculos custosos derivados de estado
[ ] useCallback para funções passadas como props a componentes memoizados
[ ] Virtualização para listas > 100 itens
[ ] Web Vitals monitorados em produção
Back-end
─────────────────────────────────────────────────────────
[ ] Índices em todos os campos usados em find(), sort(), match()
[ ] .explain('executionStats') confirma IXSCAN (não COLLSCAN)
[ ] .lean() em todas as queries de leitura
[ ] .select() buscando apenas campos necessários
[ ] Queries independentes rodando com Promise.all()
[ ] Paginação em todas as listagens (nunca busca tudo)
[ ] Cache para dados custosos e pouco mutáveis
[ ] Compressão gzip/brotli ativa
[ ] Middleware de slow requests monitorando produção
Tarefa para você
Aplique as otimizações na SPA do Módulo 6:
# 1. Meça o estado atual com Lighthouse
# Abra a SPA em produção em uma aba anônima
# Gere um relatório Lighthouse e anote os scores
# Guarde o screenshot para comparação pós-otimização
# 2. Analise o bundle com rollup-plugin-visualizer
# npm install -D rollup-plugin-visualizer
# Adicione ao vite.config.js e execute npm run build
# Identifique a maior biblioteca no mapa visual
# 3. Implemente virtualização na lista de produtos
# Se a lista tem mais de 50 itens, a diferença é visível
# npm install @tanstack/react-virtual
# 4. Adicione monitoramento de slow requests na API
# Rode a API com NODE_ENV=development
# Faça requisições e observe o header Server-Timing no DevTools
# 5. Adicione .explain() a todas as queries principais:
# Tarefa.find({ usuario: id }).explain('executionStats')
# Verifique se todas usam IXSCAN
# Adicione índices para as que usam COLLSCAN
# 6. Meça novamente com Lighthouse após as otimizações
# Compare os scores com os do passo 1
# Documente as melhorias alcançadas
Conclusão
Neste artigo você aprendeu:
- As métricas que importam — Core Web Vitals (LCP, INP, CLS) e como medi-las
- As ferramentas de diagnóstico — Lighthouse, DevTools, PageSpeed Insights
- Como analisar e reduzir o bundle size com code splitting manual
- Lazy loading avançado — não apenas páginas, mas qualquer componente pesado
- Identificar e eliminar re-renders desnecessários com React.memo, useMemo e useCallback
- Virtualização de listas longas com TanStack Virtual
- Otimização de imagens — lazy loading nativo, fetchPriority e formatos modernos
- Índices no MongoDB — a otimização de maior impacto no back-end
- .lean() e .select() para queries mais rápidas e econômicas
- Promise.all para queries independentes em paralelo
- Cache em memória para dados custosos e pouco mutáveis
- Compressão gzip/brotli para reduzir tráfego de rede
- Monitoramento de slow requests em produção
No próximo artigo vamos encerrar o Módulo 7 com a revisão completa e o projeto final — uma aplicação em produção com deploy automatizado, segurança e performance otimizados.
📚 Fontes e Referências
- Google — Core Web Vitals: https://web.dev/vitals
- web-vitals library: https://github.com/GoogleChrome/web-vitals
- PageSpeed Insights: https://pagespeed.web.dev
- WebPageTest: https://www.webpagetest.org
- TanStack Virtual: https://tanstack.com/virtual/latest
- rollup-plugin-visualizer: https://github.com/btd/rollup-plugin-visualizer
- MongoDB — Query Optimization: https://www.mongodb.com/docs/manual/core/query-optimization
- MongoDB — Indexes: https://www.mongodb.com/docs/manual/indexes
- node-cache: https://github.com/node-cache/node-cache
- compression (npm): https://github.com/expressjs/compression
- High Performance Browser Networking — Ilya Grigorik (O'Reilly, gratuito): https://hpbn.co
- Web Performance in Action — Jeremy Wagner (Manning)
- roadmap.sh — Frontend Performance: https://roadmap.sh/best-practices/frontend-performance