Disponível para freelancerEntre em contato para que eu possa ajudar seu negócio a crescer ou tirar sua ideia do papel!

Estou interessado
Técnicas de Otimização de Performance Web que Todo Frontend Deveria Conhecer

Técnicas de Otimização de Performance Web que Todo Frontend Deveria Conhecer

Um cliente uma vez me perguntou por que a página de produto dele levava 6 segundos para ficar interativa em um celular intermediário. Abri o bundle analyzer e encontrei uma biblioteca de date-picker, um conjunto de ícones inteiro e uma biblioteca de gráficos — tudo carregado em uma página que não mostrava nada disso até o usuário clicar em "Adicionar ao carrinho". Isso não é exceção. É a maioria das aplicações React que já auditei.

Trabalho de performance não é sobre micro-otimizar loops de render. Noventa por cento das vezes, é sobre enviar menos JavaScript e carregar ele no momento certo.

Para onde os milissegundos realmente vão

Antes de mexer em código, entenda o que você está otimizando. Duas métricas importam mais para o usuário:

  • Time to Interactive (TTI) — quando a página responde a cliques e toques, não só quando parece pronta
  • Largest Contentful Paint (LCP) — quando o maior elemento visível (geralmente uma imagem de destaque ou um título) termina de renderizar

As duas são destruídas pela mesma causa raiz: JavaScript demais sendo interpretado e executado antes do navegador conseguir fazer qualquer coisa útil. Um bundle de 500KB não demora só mais para baixar — ele demora mais para ser interpretado e executado, e isso trava a main thread em dispositivos fracos muito mais do que trava no seu notebook com chip M-series.

Teste com CPU limitada (4x de desaceleração no Chrome DevTools) e uma conexão 3G limitada. Sua máquina rápida mente sobre o que o usuário real experimenta.


As três alavancas que realmente fazem diferença

1. Code splitting

Você não precisa da aplicação inteira em um bundle só. Divida por rota automaticamente, e divida por feature manualmente para qualquer coisa pesada e condicional.

// ❌ Carregado em toda página, mesmo que o modal nunca abra
import { ExportModal } from '@/components/ExportModal'

function Dashboard() {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Exportar</button>
      {open && <ExportModal onClose={() => setOpen(false)} />}
    </>
  )
}
// ✅ Só é buscado quando o usuário realmente clica em Exportar
import dynamic from 'next/dynamic'

const ExportModal = dynamic(() => import('@/components/ExportModal'), {
  loading: () => <Spinner />,
})

function Dashboard() {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Exportar</button>
      {open && <ExportModal onClose={() => setOpen(false)} />}
    </>
  )
}

Essa única mudança tirou as dependências do ExportModal — uma biblioteca de PDF, nesse caso — do bundle inicial completamente. No projeto do cliente que mencionei, só isso já cortou o payload inicial de JS em 40%.

2. Entrega de imagens

Imagens costumam ser os bytes mais pesados da página, e são o que mais fácil se erra.

// ❌ Imagem em resolução completa, sem lazy loading, sem dimensões explícitas
<img src="/hero-banner.jpg" alt="Destaque do produto" />
// ✅ Responsiva, lazy por padrão abaixo da dobra, tamanho explícito evita layout shift
import Image from 'next/image'

<Image
  src="/hero-banner.jpg"
  alt="Destaque do produto"
  width={1200}
  height={600}
  priority // só para imagens acima da dobra
/>

Use priority só na imagem que determina seu LCP — geralmente a imagem de destaque. Marcar tudo como priority anula o propósito; você volta a carregar tudo de forma antecipada.

3. Scripts de terceiros

Analytics, widgets de chat, ferramentas de A/B testing — esses são muitas vezes o verdadeiro culpado, não o seu próprio código. Cada um adiciona sua própria requisição de rede, seu próprio custo de parsing de JS, e às vezes seu próprio comportamento de bloqueio de render.

// ❌ Bloqueia o parsing imediatamente, roda antes da página ficar interativa
<script src="https://widget.example.com/chat.js"></script>
// ✅ Carrega depois que a página já está interativa, não disputa a main thread no primeiro load
import Script from 'next/script'

<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />

A prop strategy do next/script te dá um controle que a maioria dos times não sabe que existe: beforeInteractive, afterInteractive ou lazyOnload. Por padrão, deixe scripts de terceiros em lazyOnload, a menos que algo dependa deles estarem prontos imediatamente.


Medindo o impacto real

Não chute. Rode o Lighthouse ou o next build com o bundle analyzer antes e depois:

ANALYZE=true npm run build

Na auditoria que mencionei antes, os números ficaram assim:

MétricaAntesDepois
Bundle JS inicial780 KB410 KB
Time to Interactive (3G, celular intermediário)6.1s2.8s
Largest Contentful Paint3.4s1.6s

Mesmas funcionalidades. Mesmo design. A única mudança foi adiar o que não precisava carregar imediatamente.


Erros comuns

  • Otimizar useMemo/useCallback antes de checar o tamanho do bundle. Performance de re-render raramente importa se a página já demorou 4 segundos para ficar interativa. Resolva o problema maior primeiro.
  • Fazer lazy-load de tudo, incluindo conteúdo acima da dobra. Se você importa dinamicamente sua seção de destaque, você atrasa exatamente o que determina seu score de LCP. Faça lazy load do que está escondido, não do que já é visível de cara.
  • Importar uma biblioteca inteira para uma função só. import _ from 'lodash' traz a biblioteca inteira mesmo que você só use debounce. Use import debounce from 'lodash/debounce' ou uma implementação nativa.
  • Ignorar o impacto de script de terceiro porque "não é meu código". O usuário não liga de quem é o código que deixou a página lenta. Audite todo script tag do mesmo jeito que você audita seu próprio bundle.

Boas práticas

  • Defina um orçamento de tamanho de bundle e reforce ele na CI. Ferramentas como bundlesize ou os avisos nativos do Next.js pegam regressões antes de irem pro ar, não depois de um usuário reclamar.
  • Use next/dynamic por padrão para qualquer coisa abaixo da dobra ou atrás de uma interação — modais, abas que não estão ativas no momento, gráficos, editores de texto rico.
  • Comprima e sirva formatos de imagem modernos. WebP ou AVIF em vez de JPEG/PNG reduz bastante o peso da imagem sem perda de qualidade visível na maioria dos casos.
  • Faça preconnect para origens de terceiros que você não consegue evitar. <link rel="preconnect" href="https://fonts.googleapis.com"> tira o handshake de DNS/TLS do caminho crítico.
  • Meça de novo depois de toda dependência "pequena" que você adicionar. Uma única biblioteca de date-picker pode adicionar 80KB. Confira antes de dar merge, não depois da aplicação já estar lenta.

O que fazer agora

Roda um bundle analyzer na sua aplicação hoje — não na próxima sprint. Encontre os três maiores chunks que não são a sua UI principal, e pergunte se eles precisam carregar no primeiro paint. Na maioria dos casos, a resposta é não, e mover eles para trás de um dynamic() ou lazyOnload leva quinze minutos por componente.

Faça isso com consistência, e o tamanho do bundle para de ser um projeto trimestral de faxina e vira um hábito que pega regressões antes delas chegarem em produção.