Performance em React/Next.js: Guia Completo de Otimização 2025

January 15, 2025 (1y ago)

Fala dev! 👋

Performance é tudo no desenvolvimento frontend moderno. Uma aplicação lenta pode resultar em 40% de abandono de usuários e impacto direto no SEO e conversões.

Neste guia completo, vou te mostrar as técnicas mais eficazes para otimizar performance em React e Next.js, com exemplos práticos e métricas reais.


📊 Por que performance importa?

Impacto real na experiência:

Métricas essenciais:


🚀 Otimizações fundamentais

1. React.memo() - Evitar re-renders desnecessários

// ❌ Componente que re-renderiza sempre
function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard renderizado') // Vai logar sempre
  
  return (
    <div className="border rounded-lg p-4">
      <h3>{product.name}</h3>
      <p>R$ {product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Adicionar ao carrinho
      </button>
    </div>
  )
}
 
// ✅ Otimizado com React.memo
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
  console.log('ProductCard renderizado') // Só loga quando props mudam
  
  return (
    <div className="border rounded-lg p-4">
      <h3>{product.name}</h3>
      <p>R$ {product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Adicionar ao carrinho
      </button>
    </div>
  )
})
 
// ✅ Com comparação customizada
const ProductCard = React.memo(
  function ProductCard({ product, onAddToCart }) {
    return (
      <div className="border rounded-lg p-4">
        <h3>{product.name}</h3>
        <p>R$ {product.price}</p>
        <button onClick={() => onAddToCart(product.id)}>
          Adicionar ao carrinho
        </button>
      </div>
    )
  },
  (prevProps, nextProps) => {
    // Só re-renderiza se product.id ou product.price mudarem
    return (
      prevProps.product.id === nextProps.product.id &&
      prevProps.product.price === nextProps.product.price
    )
  }
)

2. useMemo() - Memoizar cálculos pesados

// ❌ Cálculo executado a cada render
function ProductList({ products, filters, searchTerm }) {
  // Este filtro roda a cada render, mesmo se os dados não mudaram
  const filteredProducts = products.filter(product => {
    const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase())
    const matchesCategory = filters.category === 'all' || product.category === filters.category
    const matchesPrice = product.price >= filters.minPrice && product.price <= filters.maxPrice
    
    return matchesSearch && matchesCategory && matchesPrice
  })
  
  // Cálculo de estatísticas também roda sempre
  const totalValue = filteredProducts.reduce((sum, product) => sum + product.price, 0)
  const averagePrice = totalValue / filteredProducts.length
  
  return (
    <div>
      <div className="stats">
        <p>Total: R$ {totalValue.toFixed(2)}</p>
        <p>Média: R$ {averagePrice.toFixed(2)}</p>
      </div>
      {filteredProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}
 
// ✅ Otimizado com useMemo
function ProductList({ products, filters, searchTerm }) {
  // Só recalcula quando dependencies mudam
  const filteredProducts = useMemo(() => 
    products.filter(product => {
      const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase())
      const matchesCategory = filters.category === 'all' || product.category === filters.category
      const matchesPrice = product.price >= filters.minPrice && product.price <= filters.maxPrice
      
      return matchesSearch && matchesCategory && matchesPrice
    }), [products, filters, searchTerm]
  )
  
  // Estatísticas também memoizadas
  const { totalValue, averagePrice } = useMemo(() => {
    const total = filteredProducts.reduce((sum, product) => sum + product.price, 0)
    return {
      totalValue: total,
      averagePrice: total / filteredProducts.length
    }
  }, [filteredProducts])
  
  return (
    <div>
      <div className="stats">
        <p>Total: R$ {totalValue.toFixed(2)}</p>
        <p>Média: R$ {averagePrice.toFixed(2)}</p>
      </div>
      {filteredProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

3. useCallback() - Estabilizar funções

// ❌ Função recriada a cada render
function ProductList({ products }) {
  const [selectedProducts, setSelectedProducts] = useState([])
  
  // Esta função é recriada a cada render
  const handleProductSelect = (productId) => {
    setSelectedProducts(prev => 
      prev.includes(productId) 
        ? prev.filter(id => id !== productId)
        : [...prev, productId]
    )
  }
  
  return (
    <div>
      {products.map(product => (
        <ProductCard 
          key={product.id} 
          product={product}
          onSelect={handleProductSelect} // Nova função a cada render
        />
      ))}
    </div>
  )
}
 
// ✅ Otimizado com useCallback
function ProductList({ products }) {
  const [selectedProducts, setSelectedProducts] = useState([])
  
  // Função estável, só recria se dependencies mudam
  const handleProductSelect = useCallback((productId) => {
    setSelectedProducts(prev => 
      prev.includes(productId) 
        ? prev.filter(id => id !== productId)
        : [...prev, productId]
    )
  }, []) // Dependencies vazias = função nunca muda
  
  return (
    <div>
      {products.map(product => (
        <ProductCard 
          key={product.id} 
          product={product}
          onSelect={handleProductSelect} // Mesma referência sempre
        />
      ))}
    </div>
  )
}

🎯 Otimizações específicas do Next.js

1. Dynamic Imports - Code Splitting

// ❌ Import estático (carrega sempre)
import HeavyChart from './HeavyChart'
import DataTable from './DataTable'
 
function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart /> {/* Carrega mesmo se não for usado */}
      <DataTable />
    </div>
  )
}
 
// ✅ Dynamic imports (carrega sob demanda)
import dynamic from 'next/dynamic'
 
// Lazy loading com fallback
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <div className="animate-pulse bg-gray-200 h-64 rounded" />,
  ssr: false // Não renderiza no servidor se não precisar
})
 
const DataTable = dynamic(() => import('./DataTable'), {
  loading: () => <div className="animate-pulse bg-gray-200 h-96 rounded" />
})
 
function Dashboard() {
  const [showChart, setShowChart] = useState(false)
  
  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={() => setShowChart(!showChart)}>
        {showChart ? 'Ocultar' : 'Mostrar'} Gráfico
      </button>
      {showChart && <HeavyChart />} {/* Só carrega quando necessário */}
      <DataTable />
    </div>
  )
}

2. Image Optimization

// ❌ Tag img comum
function ProductCard({ product }) {
  return (
    <div className="card">
      <img 
        src={product.image} 
        alt={product.name}
        className="w-full h-48 object-cover"
      />
      <h3>{product.name}</h3>
    </div>
  )
}
 
// ✅ Next.js Image otimizada
import Image from 'next/image'
 
function ProductCard({ product }) {
  return (
    <div className="card">
      <Image
        src={product.image}
        alt={product.name}
        width={300}
        height={200}
        className="w-full h-48 object-cover"
        priority={product.featured} // Prioridade para imagens importantes
        placeholder="blur" // Blur enquanto carrega
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
      <h3>{product.name}</h3>
    </div>
  )
}

3. Static Generation (SSG)

// app/products/page.tsx
// ✅ Geração estática para melhor performance
export async function generateStaticParams() {
  // Gera páginas estáticas no build time
  const products = await fetch('https://api.example.com/products')
  const data = await products.json()
  
  return data.map((product) => ({
    slug: product.slug,
  }))
}
 
export async function generateMetadata({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.slug}`)
  const data = await product.json()
  
  return {
    title: data.name,
    description: data.description,
  }
}
 
export default async function ProductPage({ params }) {
  // Esta página é gerada estaticamente
  const product = await fetch(`https://api.example.com/products/${params.slug}`)
  const data = await product.json()
  
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <Image src={data.image} alt={data.name} width={500} height={300} />
    </div>
  )
}

4. Server Components

// ✅ Server Component (renderiza no servidor)
async function ProductList() {
  // Esta função roda no servidor, não no cliente
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 } // Revalida a cada hora
  })
  const data = await products.json()
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
      {data.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}
 
// ✅ Client Component (interatividade)
'use client'
 
function AddToCartButton({ productId }) {
  const [isAdding, setIsAdding] = useState(false)
  
  const handleAddToCart = async () => {
    setIsAdding(true)
    try {
      await fetch('/api/cart', {
        method: 'POST',
        body: JSON.stringify({ productId })
      })
    } finally {
      setIsAdding(false)
    }
  }
  
  return (
    <button 
      onClick={handleAddToCart}
      disabled={isAdding}
      className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
    >
      {isAdding ? 'Adicionando...' : 'Adicionar ao carrinho'}
    </button>
  )
}

📦 Bundle Optimization

1. Bundle Analyzer

# Instalar bundle analyzer
npm install --save-dev @next/bundle-analyzer
 
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
 
module.exports = withBundleAnalyzer({
  // Suas configurações do Next.js
})
 
# Rodar análise
ANALYZE=true npm run build

2. Tree Shaking

// ❌ Import de biblioteca inteira
import _ from 'lodash'
 
function Component() {
  return <div>{_.capitalize('hello world')}</div>
}
 
// ✅ Import específico
import { capitalize } from 'lodash'
 
function Component() {
  return <div>{capitalize('hello world')}</div>
}
 
// ✅ Import de biblioteca otimizada
import { capitalize } from 'lodash/capitalize'

3. Webpack Bundle Splitting

// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all',
          },
          common: {
            name: 'common',
            minChunks: 2,
            chunks: 'all',
            enforce: true,
          },
        },
      }
    }
    return config
  },
}

🔍 Monitoring e Debugging

1. React DevTools Profiler

// Componente com profiling
import { Profiler } from 'react'
 
function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  console.log('Component:', id)
  console.log('Phase:', phase)
  console.log('Actual duration:', actualDuration)
  console.log('Base duration:', baseDuration)
}
 
function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <ProductList />
    </Profiler>
  )
}

2. Performance Monitoring

// hooks/usePerformance.ts
import { useEffect } from 'react'
 
export function usePerformance() {
  useEffect(() => {
    // Core Web Vitals
    if ('web-vital' in window) {
      import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
        getCLS(console.log)
        getFID(console.log)
        getFCP(console.log)
        getLCP(console.log)
        getTTFB(console.log)
      })
    }
  }, [])
}
 
// Usar no componente principal
function App() {
  usePerformance()
  
  return (
    <div>
      {/* Seu app */}
    </div>
  )
}

3. Bundle Size Monitoring

// scripts/analyze-bundle.js
const { execSync } = require('child_process')
const fs = require('fs')
 
function analyzeBundle() {
  try {
    // Gerar bundle analysis
    execSync('ANALYZE=true npm run build', { stdio: 'inherit' })
    
    // Ler e processar resultados
    const bundleStats = JSON.parse(fs.readFileSync('.next/analyze/client.json'))
    
    console.log('📊 Bundle Analysis:')
    console.log(`Total size: ${(bundleStats.totalSize / 1024 / 1024).toFixed(2)} MB`)
    console.log(`JavaScript: ${(bundleStats.jsSize / 1024 / 1024).toFixed(2)} MB`)
    console.log(`CSS: ${(bundleStats.cssSize / 1024 / 1024).toFixed(2)} MB`)
    
  } catch (error) {
    console.error('Erro na análise:', error)
  }
}
 
analyzeBundle()

🚀 Otimizações avançadas

1. Virtual Scrolling

// Para listas grandes (1000+ itens)
import { FixedSizeList as List } from 'react-window'
 
function VirtualizedProductList({ products }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ProductCard product={products[index]} />
    </div>
  )
 
  return (
    <List
      height={600} // Altura do container
      itemCount={products.length}
      itemSize={200} // Altura de cada item
      width="100%"
    >
      {Row}
    </List>
  )
}

2. Intersection Observer

// Lazy loading de imagens
import { useIntersectionObserver } from './hooks/useIntersectionObserver'
 
function LazyImage({ src, alt, ...props }) {
  const [isLoaded, setIsLoaded] = useState(false)
  const [isInView, ref] = useIntersectionObserver()
 
  useEffect(() => {
    if (isInView && !isLoaded) {
      const img = new Image()
      img.onload = () => setIsLoaded(true)
      img.src = src
    }
  }, [isInView, src, isLoaded])
 
  return (
    <div ref={ref} {...props}>
      {isLoaded ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="animate-pulse bg-gray-200 h-48" />
      )}
    </div>
  )
}

3. Service Worker para Cache

// public/sw.js
const CACHE_NAME = 'app-cache-v1'
const urlsToCache = [
  '/',
  '/static/js/bundle.js',
  '/static/css/main.css',
]
 
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  )
})
 
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // Retorna do cache se disponível
        if (response) {
          return response
        }
        return fetch(event.request)
      })
  )
})

📊 Métricas e Ferramentas

1. Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
 
on:
  pull_request:
    branches: [main]
 
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run build
      - run: npm install -g @lhci/cli
      - run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

2. WebPageTest

// scripts/webpagetest.js
const webPageTest = require('webpagetest')
 
const wpt = new webPageTest('https://www.webpagetest.org')
 
wpt.runTest('https://meusite.com', {
  location: 'Dulles:Chrome',
  runs: 3,
  firstViewOnly: false
}, (err, data) => {
  if (err) {
    console.error('Erro:', err)
    return
  }
  
  console.log('Test ID:', data.data.testId)
  console.log('URL:', data.data.userUrl)
})

✅ Checklist de Performance

React Otimizações:

Next.js Otimizações:

Bundle Otimizações:

Monitoring:


🎯 Conclusão

Performance não é uma feature, é um requisito fundamental. As otimizações mostradas aqui podem melhorar significativamente a experiência do usuário e os resultados do negócio.

Principais benefícios:

Próximos passos:

  1. Implemente as otimizações básicas
  2. Configure monitoring de performance
  3. Teste regularmente com Lighthouse
  4. Mantenha o bundle size sob controle

Lembre-se: performance é um processo contínuo, não um destino! 🚀

Allisson Lima
Desenvolvedor Frontend | Especialista em Performance e Otimização