Inicio / Trilha 3 / Modulo 3.6
Trilha 3 - Avancado Performance

⚡ Performance e Otimizacao

Construa dashboards que carregam instantaneamente e respondem suavemente mesmo com milhares de dados.

6
Topicos
4h
Duracao
Expert
Nivel
Alto
Impacto
1

📦 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.

❌ Sem Code Splitting
Bundle unico: 2.5MB
Carrega tudo no inicio
✅ Com Code Splitting
Inicial: 400KB | Resto: sob demanda
84% menos JS inicial

🎯 Estrategias de Splitting

Route-based Splitting

Cada rota carrega seu proprio chunk. Ideal para SPAs com muitas paginas.

Component-based Splitting

Componentes pesados (graficos, editores) carregados sob demanda.

Vendor Splitting

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,
  },
});
2

📜 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.

📊
10.000 linhas
Sem virtualizacao: ~3s render
Com virtualizacao: ~50ms
💾
Memoria
Sem: 200MB+ DOM nodes
Com: ~5MB (fixo)
🎯
Scroll
Sem: Travado, jank
Com: 60fps suave

📚 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.

3

🧠 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`.

useMemo

Memoriza valores computados

const total = useMemo(() => calc(), [deps])
useCallback

Memoriza funcoes

const fn = useCallback(() => {}, [deps])
React.memo

Memoriza componentes

const Comp = memo(Component)

⚖️ Quando Usar (e Quando Nao)

✅ Use Quando
  • • Calculos pesados (sort, filter, aggregations)
  • • Componentes filhos com memo
  • • Callbacks passados para listas virtuais
  • • Referencias estaveis para useEffect
  • • Context values que mudam raramente
❌ Evite Quando
  • • 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.

⚠️ Por enquanto, continue usando hooks de memoization em codigo critico de performance.
4

⚙️ 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.

🖥️
Main Thread
UI, eventos, render
↔️
⚙️
Worker Thread
Calculos pesados
Comunicacao via postMessage (serializado)

🎯 Casos de Uso em Dashboards

📊
Agregacoes de dados
Somas, medias, pivots em datasets grandes
📈
Parsing de CSV/Excel
Importar arquivos sem travar UI
🔍
Busca e filtros complexos
Full-text search, fuzzy matching
🔐
Criptografia
Hashing, encryption de dados

💻 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} />;
}
5

📦 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.

2.5MB
Bundle nao otimizado
800KB
Com tree shaking
400KB
+ Code splitting
120KB
+ Gzip/Brotli

🔍 Ferramentas de Analise

rollup-plugin-visualizer Vite/Rollup
Treemap interativo mostrando tamanho de cada modulo
source-map-explorer Qualquer bundler
Analisa source maps para identificar codigo duplicado
bundlephobia.com Online
Verifica tamanho de pacotes npm antes de instalar

🛠️ Tecnicas de Otimizacao

Tree Shaking

Remocao automatica de codigo nao utilizado. Requer ES modules.

// Ruim: import _ from 'lodash' // 70KB
// Bom: import { debounce } from 'lodash-es' // 2KB
Substituicao de dependencias

Troque libs pesadas por alternativas leves.

moment.js (300KB) → date-fns (50KB) → dayjs (2KB)
lodash (70KB) → remeda (tree-shakeable)
Dynamic imports condicionais

Carregue features apenas quando necessario.

const pdf = user.canExport ? await import('pdf-lib') : null;

💻 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',
    },
  },
});
6

📊 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.

LCP
Largest Contentful Paint
Tempo ate o maior elemento renderizar
Bom: <2.5s OK: <4s
INP
Interaction to Next Paint
Responsividade a interacoes
Bom: <200ms OK: <500ms
CLS
Cumulative Layout Shift
Estabilidade visual da pagina
Bom: <0.1 OK: <0.25

🎯 Otimizacoes por Metrica

LCP - Melhorar carregamento
  • • Preload de recursos criticos: <link rel="preload">
  • • Otimizar imagens (WebP, lazy load, srcset)
  • • Server-side rendering ou static generation
  • • CDN para assets estaticos
INP - Melhorar interatividade
  • • Evitar long tasks (>50ms no main thread)
  • • Usar Web Workers para processamento pesado
  • • Debounce/throttle em handlers de eventos
  • • Virtualizar listas longas
CLS - Melhorar estabilidade
  • • 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} />;
}

📊 Dashboard de Performance

LCP (p75)
1.8s
INP (p75)
145ms
CLS
0.05
Bundle Size
142KB
% de usuarios com "Bom" LCP nos ultimos 7 dias

📝 Resumo do Modulo

Code Splitting - Lazy loading de rotas e componentes, reducao de bundle inicial
Virtualizacao - Renderizar milhares de itens sem impacto na performance
Memoization - useMemo, useCallback, React.memo para evitar re-renders
Web Workers - Processamento pesado sem bloquear a UI principal
Bundle Optimization - Tree shaking, compression, analise de dependencias
Core Web Vitals - LCP, INP, CLS e como otimizar cada metrica