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:
- ⚡ 3 segundos = 40% de usuários abandonam
- 📱 Mobile = 70% do tráfego web
- 🔍 SEO = Core Web Vitals afetam ranking
- 💰 Conversão = 1s de melhoria = +7% conversões
Métricas essenciais:
- FCP (First Contentful Paint) < 1.8s
- LCP (Largest Contentful Paint) < 2.5s
- CLS (Cumulative Layout Shift) < 0.1
- FID (First Input Delay) < 100ms
🚀 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 build2. 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:
- Usar React.memo() em componentes puros
- Implementar useMemo() para cálculos pesados
- Usar useCallback() para funções estáveis
- Evitar re-renders desnecessários
- Implementar lazy loading de componentes
Next.js Otimizações:
- Usar Image component do Next.js
- Implementar SSG quando possível
- Usar Server Components
- Configurar bundle splitting
- Otimizar imports
Bundle Otimizações:
- Analisar bundle size
- Implementar tree shaking
- Usar dynamic imports
- Configurar webpack splitting
- Monitorar dependências
Monitoring:
- Configurar Core Web Vitals
- Implementar Lighthouse CI
- Monitorar bundle size
- Configurar performance budgets
- Testar em diferentes dispositivos
🎯 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:
- ⚡ Velocidade: 3-5x mais rápido
- 📱 Mobile: Melhor experiência em dispositivos móveis
- 🔍 SEO: Melhor ranking no Google
- 💰 Conversão: Mais vendas e engajamento
Próximos passos:
- Implemente as otimizações básicas
- Configure monitoring de performance
- Teste regularmente com Lighthouse
- 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