⚡ Performance e Otimizacao
Construa dashboards que carregam instantaneamente e respondem suavemente mesmo com milhares de dados.
📦 Lazy Loading e Code Splitting
Carregue apenas o codigo necessario, quando necessario
📋 O que e Code Splitting
Code Splitting divide seu bundle JavaScript em pedacos menores (chunks) que sao carregados sob demanda. Em vez de carregar toda a aplicacao no primeiro acesso, carrega apenas o necessario para a pagina atual.
🎯 Estrategias de Splitting
Cada rota carrega seu proprio chunk. Ideal para SPAs com muitas paginas.
Componentes pesados (graficos, editores) carregados sob demanda.
Separa bibliotecas de terceiros para melhor cache.
💻 Codigo: React Lazy Loading
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Lazy load de rotas
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Reports = lazy(() => import('./pages/Reports'));
// Lazy load com retry
const ChartComponent = lazy(() =>
import('./components/HeavyChart').catch(() => {
// Retry em caso de falha de rede
return new Promise(resolve => {
setTimeout(() => resolve(import('./components/HeavyChart')), 1500);
});
})
);
// Preload em hover (prefetch)
const preloadAnalytics = () => {
import('./pages/Analytics');
};
// Loading fallback customizado
const PageLoader = () => (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full" />
</div>
);
// Componente com Suspense
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route
path="/analytics"
element={
<div onMouseEnter={preloadAnalytics}>
<Analytics />
</div>
}
/>
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
// Named exports para chunks nomeados
const DashboardPage = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
);
// Prefetch via link tags
<link rel="prefetch" href="/static/js/analytics.chunk.js" />
⚙️ Configuracao Vite para Splitting
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendors em chunks separados
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-charts': ['recharts', 'd3'],
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'vendor-query': ['@tanstack/react-query'],
},
},
},
// Divide chunks grandes
chunkSizeWarningLimit: 500,
},
});
📜 Virtualizacao de Listas
Renderize milhares de itens sem travar o browser
📋 Por que Virtualizar
Listas virtuais renderizam apenas os itens visiveis na viewport, mais um buffer. Em vez de criar 10.000 elementos DOM para uma tabela, cria apenas ~50 e recicla conforme o scroll.
📚 Bibliotecas Populares
| Biblioteca | Uso | Bundle | Features |
|---|---|---|---|
| @tanstack/virtual | Headless, flexivel | ~3KB | ✅ Grid, ✅ Dynamic, ✅ Infinite |
| react-window | Simples, rapido | ~6KB | ✅ Grid, ⚠️ Fixed size |
| react-virtuoso | Full-featured | ~15KB | ✅ Grid, ✅ Dynamic, ✅ Groups |
| AG Grid | Enterprise tables | ~200KB+ | ✅ Tudo (sort, filter, pivot) |
💻 Codigo: TanStack Virtual
import { useVirtualizer } from '@tanstack/react-virtual';
interface DataRow {
id: string;
name: string;
value: number;
status: 'active' | 'inactive';
}
function VirtualizedTable({ data }: { data: DataRow[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // Altura estimada de cada linha
overscan: 5, // Renderiza 5 itens extras acima/abaixo
});
const virtualItems = virtualizer.getVirtualItems();
return (
<div
ref={parentRef}
className="h-[600px] overflow-auto"
>
{/* Container com altura total para scroll correto */}
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{/* Apenas itens visiveis sao renderizados */}
{virtualItems.map((virtualRow) => {
const row = data[virtualRow.index];
return (
<div
key={row.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
className="flex items-center border-b border-dark-700 px-4"
>
<span className="w-1/3">{row.name}</span>
<span className="w-1/3">{row.value.toLocaleString()}</span>
<span className="w-1/3">
<span className={`px-2 py-1 rounded text-xs ${
row.status === 'active'
? 'bg-green-500/20 text-green-400'
: 'bg-red-500/20 text-red-400'
}`}>
{row.status}
</span>
</span>
</div>
);
})}
</div>
</div>
);
}
// Com alturas dinamicas
const virtualizerDynamic = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
measureElement: (element) => element.getBoundingClientRect().height,
});
💡 Dica de Performance
Para tabelas com muitas colunas, considere virtualizar tambem horizontalmente (grid virtual 2D). Use `columnVirtualizer` junto com `rowVirtualizer` para renderizar apenas as celulas visiveis.
🧠 Memoization e Re-renders
Evite calculos e renderizacoes desnecessarias
📋 O que e Memoization
Memoization e uma tecnica de otimizacao que armazena resultados de funcoes/componentes para evitar recomputacao quando os inputs nao mudam. Em React, temos `useMemo`, `useCallback` e `React.memo`.
Memoriza valores computados
Memoriza funcoes
Memoriza componentes
⚖️ Quando Usar (e Quando Nao)
- • Calculos pesados (sort, filter, aggregations)
- • Componentes filhos com memo
- • Callbacks passados para listas virtuais
- • Referencias estaveis para useEffect
- • Context values que mudam raramente
- • Operacoes triviais (soma, concat)
- • Componentes que sempre re-renderizam
- • Deps mudam toda renderizacao
- • Otimizacao prematura
- • Valores primitivos simples
💻 Codigo: Patterns de Memoization
// 1. useMemo para calculos pesados
function DashboardMetrics({ data, filters }: Props) {
// ✅ Recalcula apenas quando data ou filters mudam
const filteredData = useMemo(() => {
console.log('Filtering...'); // Para debug
return data
.filter(item => matchesFilters(item, filters))
.sort((a, b) => b.value - a.value);
}, [data, filters]);
const aggregations = useMemo(() => ({
total: filteredData.reduce((sum, item) => sum + item.value, 0),
average: filteredData.length ? filteredData.reduce((s, i) => s + i.value, 0) / filteredData.length : 0,
max: Math.max(...filteredData.map(i => i.value)),
min: Math.min(...filteredData.map(i => i.value)),
}), [filteredData]);
return <MetricsDisplay data={aggregations} />;
}
// 2. useCallback para handlers estaveis
function DataTable({ data, onRowClick, onSort }: Props) {
// ✅ Callback estavel para itens memorizados
const handleRowClick = useCallback((id: string) => {
onRowClick(id);
}, [onRowClick]);
// ✅ Factory de callbacks para itens de lista
const getRowHandler = useCallback((id: string) => () => {
handleRowClick(id);
}, [handleRowClick]);
return (
<VirtualList
data={data}
renderItem={({ item }) => (
<MemoizedRow
key={item.id}
data={item}
onClick={getRowHandler(item.id)}
/>
)}
/>
);
}
// 3. React.memo com comparador customizado
interface ChartProps {
data: DataPoint[];
config: ChartConfig;
}
const MemoizedChart = memo(function Chart({ data, config }: ChartProps) {
return <ResponsiveContainer>{/* ... */}</ResponsiveContainer>;
}, (prevProps, nextProps) => {
// Comparacao profunda apenas nos campos relevantes
return (
prevProps.data.length === nextProps.data.length &&
prevProps.data.every((d, i) => d.value === nextProps.data[i].value) &&
prevProps.config.type === nextProps.config.type
);
});
// 4. Context otimizado - separar estado que muda frequentemente
const DashboardContext = createContext<DashboardState>(null!);
const DashboardActionsContext = createContext<DashboardActions>(null!);
function DashboardProvider({ children }: PropsWithChildren) {
const [state, dispatch] = useReducer(reducer, initialState);
// ✅ Actions nunca mudam, evita re-render em consumers
const actions = useMemo(() => ({
setFilter: (f: Filter) => dispatch({ type: 'SET_FILTER', payload: f }),
refresh: () => dispatch({ type: 'REFRESH' }),
}), []);
return (
<DashboardContext.Provider value={state}>
<DashboardActionsContext.Provider value={actions}>
{children}
</DashboardActionsContext.Provider>
</DashboardContext.Provider>
);
}
🔮 React Compiler (Futuro)
O React Compiler (anteriormente React Forget) automatiza memoization. Quando disponivel em producao, muito do codigo manual de useMemo/useCallback se tornara desnecessario.
⚙️ Web Workers
Processamento pesado sem bloquear a UI
📋 O que sao Web Workers
Web Workers permitem executar JavaScript em threads separadas do main thread. Isso evita que operacoes pesadas bloqueiem a UI, mantendo a interface responsiva durante processamentos intensivos.
🎯 Casos de Uso em Dashboards
💻 Codigo: Worker com Comlink
// analytics.worker.ts
import { expose } from 'comlink';
interface DataPoint {
timestamp: Date;
value: number;
category: string;
}
const analyticsWorker = {
// Agregacao pesada
async aggregate(data: DataPoint[], groupBy: 'day' | 'week' | 'month') {
const groups = new Map<string, number[]>();
for (const point of data) {
const key = formatDate(point.timestamp, groupBy);
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(point.value);
}
return Array.from(groups.entries()).map(([key, values]) => ({
period: key,
sum: values.reduce((a, b) => a + b, 0),
avg: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
count: values.length,
}));
},
// Busca fuzzy em dataset grande
async fuzzySearch(data: DataPoint[], query: string, threshold = 0.6) {
const results: Array<DataPoint & { score: number }> = [];
for (const item of data) {
const score = calculateFuzzyScore(item.category, query);
if (score >= threshold) {
results.push({ ...item, score });
}
}
return results.sort((a, b) => b.score - a.score);
},
// Parse de CSV grande
async parseCSV(csvString: string): Promise<DataPoint[]> {
const lines = csvString.split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return {
timestamp: new Date(values[0]),
value: parseFloat(values[1]),
category: values[2],
};
});
},
};
expose(analyticsWorker);
// ================================
// useAnalyticsWorker.ts - Hook
// ================================
import { wrap } from 'comlink';
type AnalyticsWorker = typeof analyticsWorker;
let worker: Worker | null = null;
let api: AnalyticsWorker | null = null;
function getWorker(): AnalyticsWorker {
if (!worker) {
worker = new Worker(
new URL('./analytics.worker.ts', import.meta.url),
{ type: 'module' }
);
api = wrap<AnalyticsWorker>(worker);
}
return api!;
}
export function useAnalyticsWorker() {
const workerApi = useMemo(() => getWorker(), []);
const aggregate = useCallback(async (data: DataPoint[], groupBy: GroupBy) => {
return workerApi.aggregate(data, groupBy);
}, [workerApi]);
const search = useCallback(async (data: DataPoint[], query: string) => {
return workerApi.fuzzySearch(data, query);
}, [workerApi]);
return { aggregate, search };
}
// ================================
// Uso no componente
// ================================
function AnalyticsDashboard() {
const { aggregate } = useAnalyticsWorker();
const [aggregated, setAggregated] = useState<AggregatedData[]>([]);
useEffect(() => {
// Nao bloqueia a UI
aggregate(rawData, 'month').then(setAggregated);
}, [rawData, aggregate]);
return <Chart data={aggregated} />;
}
📦 Otimizacao de Bundle
Reduza o tamanho do JavaScript enviado ao browser
📋 Por que Otimizar Bundle
Cada KB de JavaScript adiciona tempo de download, parse e execucao. Em conexoes 3G, 100KB extras podem significar 1-2 segundos a mais de carregamento.
🔍 Ferramentas de Analise
🛠️ Tecnicas de Otimizacao
Remocao automatica de codigo nao utilizado. Requer ES modules.
// Bom: import { debounce } from 'lodash-es' // 2KB
Troque libs pesadas por alternativas leves.
lodash (70KB) → remeda (tree-shakeable)
Carregue features apenas quando necessario.
💻 Codigo: Configuracao Vite Otimizada
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import { compression } from 'vite-plugin-compression2';
export default defineConfig({
plugins: [
react(),
compression({ algorithm: 'brotliCompress' }),
visualizer({ open: true, gzipSize: true }),
],
build: {
target: 'es2020',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true,
},
},
rollupOptions: {
output: {
manualChunks: (id) => {
// Vendors pesados em chunks separados
if (id.includes('node_modules')) {
if (id.includes('recharts') || id.includes('d3')) {
return 'vendor-charts';
}
if (id.includes('react')) {
return 'vendor-react';
}
return 'vendor';
}
},
},
},
// Otimizacoes de CSS
cssCodeSplit: true,
// Gera source maps apenas em staging
sourcemap: process.env.NODE_ENV === 'staging',
},
// Aliases para versoes otimizadas
resolve: {
alias: {
'lodash': 'lodash-es',
},
},
});
📊 Monitoring e Core Web Vitals
Metricas de performance real e como otimizar
📋 Core Web Vitals
Metricas do Google que medem a experiencia real do usuario. Afetam SEO e sao essenciais para aplicacoes de qualidade.
🎯 Otimizacoes por Metrica
- • Preload de recursos criticos: <link rel="preload">
- • Otimizar imagens (WebP, lazy load, srcset)
- • Server-side rendering ou static generation
- • CDN para assets estaticos
- • Evitar long tasks (>50ms no main thread)
- • Usar Web Workers para processamento pesado
- • Debounce/throttle em handlers de eventos
- • Virtualizar listas longas
- • Reservar espaco para imagens (width/height ou aspect-ratio)
- • Evitar injecao de conteudo acima do existente
- • Fontes com font-display: swap e preload
- • Skeletons com tamanho fixo
💻 Codigo: Monitoramento com web-vitals
// performance.ts
import { onLCP, onINP, onCLS, onFCP, onTTFB, Metric } from 'web-vitals';
interface PerformanceReport {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
entries: PerformanceEntry[];
}
// Envia metricas para analytics
function sendToAnalytics(metric: Metric) {
const report: PerformanceReport = {
name: metric.name,
value: metric.value,
rating: metric.rating,
entries: metric.entries,
};
// Beacon API para envio confiavel mesmo em page unload
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/performance', JSON.stringify(report));
} else {
fetch('/api/analytics/performance', {
method: 'POST',
body: JSON.stringify(report),
keepalive: true,
});
}
// Log em desenvolvimento
if (process.env.NODE_ENV === 'development') {
console.log(`[${metric.name}]`, metric.value.toFixed(2), metric.rating);
}
}
// Registra todas as metricas
export function initPerformanceMonitoring() {
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
// Custom metrics para dashboards
export function measureDashboardLoad(dashboardId: string) {
const start = performance.now();
return {
markDataLoaded: () => {
performance.measure('dashboard-data-load', {
start,
end: performance.now(),
});
},
markRenderComplete: () => {
const duration = performance.now() - start;
sendToAnalytics({
name: 'dashboard-full-load',
value: duration,
rating: duration < 2000 ? 'good' : duration < 4000 ? 'needs-improvement' : 'poor',
id: dashboardId,
} as any);
},
};
}
// Uso
function Dashboard({ id }: { id: string }) {
const metrics = useRef(measureDashboardLoad(id));
const { data } = useQuery({
queryKey: ['dashboard', id],
queryFn: fetchDashboard,
onSuccess: () => metrics.current.markDataLoaded(),
});
useEffect(() => {
if (data) {
// Aguarda render completo
requestAnimationFrame(() => {
metrics.current.markRenderComplete();
});
}
}, [data]);
return <DashboardContent data={data} />;
}