Front End

24 mar, 2026

Performance de verdade no front-end: o que importa além do Lighthouse

Publicidade

Todo mundo já comemorou um 95+ no Lighthouse. E todo mundo já abriu o site em produção… e sentiu ele lento.

Esse é o ponto: Lighthouse não representa o mundo real. Usuários reais têm:

  • CPU limitada
  • rede instável
  • múltiplas abas abertas

Se você está otimizando só para laboratório, você está resolvendo o problema errado.

Web Vitals: o que realmente mede a experiência

Se você quer jogar no nível sênior, precisa sair do “achismo”.

Precisa medir.

LCP (Largest Contentful Paint)

Tempo até o principal conteúdo aparecer.

Problema clássico: imagens pesadas ou bloqueio de render.

CLS (Cumulative Layout Shift)

A tela “pulando” enquanto carrega.

Geralmente causado por imagens sem dimensão definida.

INP (Interaction to Next Paint)

Tempo de resposta a interações.

👉 Esse é o que mais denuncia apps React mal otimizados.

Medindo Web Vitals na prática

Aqui vai um exemplo real de coleta de métricas no app:

// web-vitals.ts
import { onLCP, onCLS, onINP } from 'web-vitals';

function sendToAnalytics(metric: any) {
  console.log('[Web Vitals]', metric);

  // Exemplo: enviar para backend ou ferramenta de analytics
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify(metric),
  });
}

export function reportWebVitals() {
  onLCP(sendToAnalytics);
  onCLS(sendToAnalytics);
  onINP(sendToAnalytics);
}
// main.tsx ou index.tsx
import { reportWebVitals } from './web-vitals';

reportWebVitals();

Agora você não depende mais de ferramenta externa.

Você mede no mundo real.

Code Splitting: carregando só o necessário

Um dos maiores erros em apps grandes:

👉 carregar tudo upfront.

Anti-pattern real

// App.tsx
import Dashboard from './pages/Dashboard';
import Reports from './pages/Reports';
import Settings from './pages/Settings';

export default function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/reports" element={<Reports />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Router>
  );
}

Aqui você já carregou TODAS as páginas no primeiro load.

Code splitting por rota (correto)

// App.tsx
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));
const Settings = lazy(() => import('./pages/Settings'));

export default function App() {
  return (
    <Router>
      <Suspense fallback={<div>Carregando...</div>}>
        <Routes>
          <Route path="/" element={<Dashboard />} />
          <Route path="/reports" element={<Reports />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

 

O detalhe que pouca gente fala

Se você exagerar no splitting:

  • cria dezenas de requests
  • gera waterfall
  • piora o tempo total

👉 Sênior não divide tudo. Divide com estratégia.


Streaming SSR: percepção > carregamento

Frameworks como Next.js mudaram o jogo.

A ideia é simples:

👉 não espere tudo ficar pronto.

Exemplo com React Server Components + Suspense

// app/page.tsx (Next.js)
import { Suspense } from 'react';
import UserProfile from './UserProfile';
import Posts from './Posts';

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<p>Carregando perfil...</p>}>
        <UserProfile />
      </Suspense>

      <Suspense fallback={<p>Carregando posts...</p>}>
        <Posts />
      </Suspense>
    </div>
  );
}

O que acontece aqui

  • o HTML começa a ser enviado antes de tudo estar pronto
  • partes da UI aparecem aos poucos
  • usuário percebe velocidade maior

👉 Isso melhora LCP sem necessariamente reduzir tempo total

React: onde a performance realmente quebra

Grande parte dos gargalos está na renderização.

No React.Re-render desnecessário (caso real)

// Lista grande sem otimização
function ProductList({ products }) {
  const [cart, setCart] = useState([]);

  function addToCart(product) {
    setCart([...cart, product]);
  }

  return (
    <div>
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAdd={addToCart}
        />
      ))}
    </div>
  );
}

Problema:

👉 toda vez que o carrinho muda, TODOS os itens re-renderizam

Otimizando com memo + useCallback

const ProductItem = React.memo(function ProductItem({ product, onAdd }) {
  console.log('Render item:', product.name);

  return (
    <div>
      <span>{product.name}</span>
      <button onClick={() => onAdd(product)}>Adicionar</button>
    </div>
  );
});

function ProductList({ products }) {
  const [cart, setCart] = useState([]);

  const addToCart = useCallback((product) => {
    setCart(prev => [...prev, product]);
  }, []);

  return (
    <div>
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAdd={addToCart}
        />
      ))}
    </div>
  );
}

Agora:

  • re-render reduz drasticamente
  • lista escala melhor

Virtualização: essencial para listas grandes

Sem virtualização:

👉 você renderiza tudo
👉 mesmo o que não está visível

Exemplo com react-window

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style, data }) => {
  const item = data[index];

  return (
    <div style={style}>
      {item.name}
    </div>
  );
};

export default function VirtualizedList({ items }) {
  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={50}
      width={300}
      itemData={items}
    >
      {Row}
    </List>
  );
}

Resultado prático

  • 10.000 itens → renderiza ~10-20
  • ganho absurdo de performance
  • scroll suave

Batching e controle de render

React 18 já faz batching automático, mas entender isso evita bugs.

Exemplo real de problema

function Example() {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);

  function handleClick() {
    setLoading(true);

    setTimeout(() => {
      setCount(c => c + 1);
      setLoading(false);
    }, 1000);
  }

  return (
    <button onClick={handleClick}>
      {loading ? 'Carregando...' : count}
    </button>
  );
}

Dependendo do contexto, isso pode gerar múltiplos renders.

Abordagem mais controlada

import { startTransition } from 'react';

function handleClick() {
  setLoading(true);

  setTimeout(() => {
    startTransition(() => {
      setCount(c => c + 1);
      setLoading(false);
    });
  }, 1000);
}

Agora você controla prioridade de render.

Benchmark: o diferencial de quem é sênior

Aqui é onde você separa discurso de resultado.

Exemplo real de melhoria

Depois de aplicar:

  • code splitting por rota
  • memo em lista crítica
  • virtualização

Resultado:

  • Bundle: 1.3MB → 620KB
  • LCP: 4.5s → 2.2s
  • INP: 300ms → 110ms

O erro clássico: otimizar sem medir

Você coloca:

  • memo em tudo
  • lazy em tudo
  • abstrações complexas

Sem saber se precisa.

Resultado

  • código mais difícil
  • bugs mais difíceis
  • ganho irrelevante

Conclusão: performance é percepção

Usuário não vê score.

Usuário sente:

  • demora
  • travamento
  • lag

Se você quer performance de verdade:

  • meça no mundo real
  • otimize gargalos reais
  • valide com dados

Porque no fim…

👉 performance boa é invisível
👉 performance ruim é inesquecível