Javascript

Performance em aplicações web Já leu

21 min de leitura

Performance em aplicações web
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. Performan

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
Comentários

Mais em Javascript

Revisão do Módulo 8 + Projeto: Refatoração com Clean Architecture
Revisão do Módulo 8 + Projeto: Refatoração com Clean Architecture

Artigo 49 — Revisão do Módulo 8 + Projeto: Refatoração com Clean Architecture...

Async/Await: escrevendo código assíncrono de forma limpa
Async/Await: escrevendo código assíncrono de forma limpa

As Promises resolveram o Callback Hell. Mas encadear muitos&nbsp;.then() aind...

Mini Projeto: Quiz Interativo
Mini Projeto: Quiz Interativo

Chegamos ao fim do M&oacute;dulo 2. Em sete artigos voc&ecirc; aprendeu a man...