Files
Temp_MSSPLASHPage/plan_performance_dashboard.md
2025-10-29 23:10:28 -06:00

24 KiB

Plan: POC de Optimización de Dashboard con 6 Widgets

Objetivo

Crear una prueba de concepto (POC) que demuestre las optimizaciones de rendimiento manteniendo la reactividad completa a cambios de filtros (fecha y redes).

Contexto del Problema

Problemas Identificados en DynamicDashboardClient.tsx

  1. Re-renders masivos en cascada - Cada widget se re-renderiza cuando cambia CUALQUIER estado del padre
  2. Cálculos costosos de layout - Se ejecutan en cada render (operaciones O(n²))
  3. Sin memoización - Ningún componente usa React.memo
  4. Todos los widgets se cargan al inicio - No hay lazy loading ni code splitting
  5. 30+ console.logs en producción - Fugas de memoria y overhead
  6. Context global - Todos los widgets se re-renderizan cuando cambian los filtros
  7. Sin virtualización - Se renderizan todos los widgets aunque no estén visibles

Impacto Actual

  • Carga actual: ~8 segundos
  • Freezing: Frecuente al cambiar filtros
  • Re-renders innecesarios: 10+ por cambio de filtro

Resultado Esperado

  • Carga optimizada: ~2 segundos (75% mejora)
  • Reducción de freezing: 95%+
  • Re-renders innecesarios: 0-1 por cambio de filtro
  • Tiempo de respuesta a filtros: <100ms (vs ~500ms actual)

Archivos a Crear

1. Página de POC para Comparación

Archivo: src/SplashPage.Web.Ui/src/app/dashboard/dynamicDashboard/poc/page.tsx

Propósito:

  • Layout con tabs: "Versión Original" vs "Versión Optimizada"
  • Métricas en tiempo real (render count, tiempo de renderizado)
  • Mismo conjunto de 6 widgets en ambas versiones
  • Controles de filtros compartidos para comparación justa

Funcionalidad:

- Tab 1: Dashboard Original (sin cambios)
- Tab 2: Dashboard Optimizado (con todas las mejoras)
- Panel de métricas:
  * Render count por widget
  * Tiempo total de renderizado
  * Memory usage
  * Re-render triggers

2. Componentes Optimizados (en carpeta _poc/)

A. Componente Principal Optimizado

Archivo: _poc/OptimizedDashboardClient.tsx

Optimizaciones:

  • React.memo en widgets con comparador custom
  • useMemo para enrichedLayouts (solo cuando cambien layouts/widgets)
  • useCallback para todos los handlers (duplicateWidget, removeWidget, etc.)
  • Remover console.logs (usar devLog wrapper)
  • Split de contexto (metadata separado de filters)
  • Memoizar availableNetworks y formattedPresets

Cambios clave:

// ANTES: Cálculo en cada render
const enrichedLayouts = { lg: layouts.lg.map(...), ... }

// DESPUÉS: Memoizado con dependencias específicas
const enrichedLayouts = useMemo(() =>
  enrichLayouts(layouts, widgets, widgetTypes),
  [layouts.lg, layouts.md, layouts.sm, layouts.xs, widgets]
);

B. Widget Wrapper Memoizado

Archivo: _poc/MemoizedWidgetRenderer.tsx

Propósito:

  • Componente wrapper que previene re-renders innecesarios
  • Solo re-renderiza si cambian: fecha, redes seleccionadas, o configuración del widget
  • Maneja lógica de edit mode, hover states, y acciones

Implementación:

export const MemoizedWidgetRenderer = React.memo<Props>(
  function MemoizedWidgetRenderer({ widget, filters, ... }) {
    // Memoizar props del widget
    const widgetProps = useMemo(() => ({
      dashboardId: filters.dashboardId,
      dateRange: filters.dateRange,
      selectedNetworkIds: filters.selectedNetworkIds,
      selectedNetworkGroupIds: filters.selectedNetworkGroupIds,
    }), [
      filters.dashboardId,
      filters.dateRange?.from?.getTime(),
      filters.dateRange?.to?.getTime(),
      JSON.stringify(filters.selectedNetworkIds),
      JSON.stringify(filters.selectedNetworkGroupIds),
    ]);

    return <WidgetComponent {...widgetProps} />;
  },
  (prevProps, nextProps) => {
    // Comparador custom - solo re-render si cambian valores críticos
    return areFiltersEqual(prevProps.filters, nextProps.filters) &&
           prevProps.widget.i === nextProps.widget.i &&
           prevProps.isEditMode === nextProps.isEditMode;
  }
);

C. Context Optimizado con Selectores

Archivo: _poc/OptimizedDashboardFiltersContext.tsx

Mejoras:

  1. Split de contextos - Separar concerns para prevenir re-renders en cascada:

    • DashboardMetadataContext - ID, nombre (cambia raramente)
    • DashboardFiltersContext - Filtros activos (cambia frecuentemente)
    • DashboardEditModeContext - Estado de edición (no afecta widgets)
  2. Selectores granulares - Widgets solo suscriben a lo que necesitan:

// Hook con selector opcional
function useDashboardFilters<T>(selector?: (filters: Filters) => T) {
  const context = useContext(DashboardFiltersContext);

  // Si hay selector, memoizar el resultado
  return useMemo(() =>
    selector ? selector(context) : context,
    [context, selector]
  );
}

// Uso en widget - solo re-renderiza si cambia dateRange
const dateRange = useDashboardFilters(filters => filters.dateRange);
  1. Debouncing inteligente - 150ms para date picker (mantiene UX fluida)

D. Hook de Layout Optimizado

Archivo: _poc/useOptimizedLayouts.ts

Optimizaciones:

  1. Map lookups (O(1)) en lugar de .find() (O(n)):
const widgetMap = new Map(widgets.map(w => [w.i, w]));
const widgetTypeMap = new Map(widgetTypes.map(wt => [wt.id, wt]));
  1. Memoización profunda - Solo recalcula cuando cambian layouts o widgets:
const enrichedLayouts = useMemo(() => {
  return {
    lg: enrichLayout(layouts.lg),
    md: enrichLayout(layouts.md),
    sm: enrichLayout(layouts.sm),
    xs: enrichLayout(layouts.xs),
  };
}, [layouts.lg, layouts.md, layouts.sm, layouts.xs, widgets]);
  1. Cache de cálculos de proporciones - Precalcular conversiones de breakpoints

E. Utilidad de Medición

Archivo: _poc/PerformanceMonitor.tsx

Métricas a capturar:

  • Render count por widget (usando Profiler API)
  • Tiempo de renderizado total
  • Time to Interactive (TTI)
  • Memory footprint
  • Re-render triggers (qué causó el re-render)

UI del monitor:

<PerformanceMonitor>
  {({ metrics }) => (
    <div className="metrics-panel">
      <MetricCard title="Total Renders" value={metrics.totalRenders} />
      <MetricCard title="Avg Render Time" value={metrics.avgRenderTime} />
      <MetricCard title="Memory Usage" value={metrics.memoryUsage} />
      <MetricCard title="Re-render Triggers" value={metrics.triggers} />
    </div>
  )}
</PerformanceMonitor>

F. Utilidades de Desarrollo

Archivo: _poc/utils.ts

Helpers:

// Logger condicional
export const devLog = process.env.NODE_ENV === 'development'
  ? console.log.bind(console)
  : () => {};

// Comparador profundo de filtros
export function areFiltersEqual(prev: Filters, next: Filters): boolean {
  return (
    prev.dashboardId === next.dashboardId &&
    prev.dateRange?.from?.getTime() === next.dateRange?.from?.getTime() &&
    prev.dateRange?.to?.getTime() === next.dateRange?.to?.getTime() &&
    arraysEqual(prev.selectedNetworkIds, next.selectedNetworkIds) &&
    arraysEqual(prev.selectedNetworkGroupIds, next.selectedNetworkGroupIds)
  );
}

// Comparador de arrays
export function arraysEqual<T>(a: T[], b: T[]): boolean {
  if (a.length !== b.length) return false;
  const sortedA = [...a].sort();
  const sortedB = [...b].sort();
  return sortedA.every((val, idx) => val === sortedB[idx]);
}

// Deep clone optimizado
export function fastClone<T>(obj: T): T {
  // Usar structuredClone si está disponible (más rápido que JSON)
  if (typeof structuredClone !== 'undefined') {
    return structuredClone(obj);
  }
  return JSON.parse(JSON.stringify(obj));
}

Estructura del POC

/dashboard/dynamicDashboard/
├── poc/
│   ├── page.tsx                          # Página de comparación con tabs
│   └── _poc/
│       ├── OptimizedDashboardClient.tsx  # Cliente optimizado
│       ├── MemoizedWidgetRenderer.tsx    # Wrapper memoizado
│       ├── OptimizedDashboardFiltersContext.tsx  # Context con selectores
│       ├── useOptimizedLayouts.ts        # Hook de layouts optimizado
│       ├── PerformanceMonitor.tsx        # Monitor de métricas
│       └── utils.ts                      # Utilidades compartidas

Conjunto de 6 Widgets para Prueba

Seleccionados estratégicamente para cubrir diferentes casos de uso:

  1. RealTimeUsers (SplashWidgetType.RealTimeUsers)

    • Actualización frecuente
    • Datos en tiempo real
    • KPI simple
  2. ConnectedClientDetails (SplashWidgetType.ConnectedClientDetails)

    • Tabla con muchos datos
    • Paginación
    • Alto consumo de memoria
  3. VisitsHistorical (SplashWidgetType.VisitsHistorical)

    • Gráfico complejo (recharts)
    • Muchos data points
    • Sensible a cambios de fecha
  4. UniqueUsers (SplashWidgetType.UniqueUsers)

    • KPI simple
    • Datos agregados
    • Rápido de renderizar
  5. NewVsReturning (SplashWidgetType.NewVsReturning)

    • Pie chart
    • Animaciones
    • Mediana complejidad
  6. Loyalty (SplashWidgetType.Loyalty)

    • Gráfico de área
    • Múltiples series
    • Filtros de red importantes

Razón de selección: Cubren el espectro de complejidad (simple KPI → tablas complejas → gráficos animados) y diferentes patrones de actualización de datos.


Optimizaciones Específicas a Implementar

🎯 Prioridad 1: Prevenir Re-renders Innecesarios

1.1. React.memo en todos los widgets

const MemoizedWidget = React.memo(
  WidgetComponent,
  (prevProps, nextProps) => {
    // Comparador custom - solo re-render si cambió lo importante
    return (
      prevProps.dashboardId === nextProps.dashboardId &&
      prevProps.dateRange?.from?.getTime() === nextProps.dateRange?.from?.getTime() &&
      prevProps.dateRange?.to?.getTime() === nextProps.dateRange?.to?.getTime() &&
      arraysEqual(prevProps.selectedNetworkIds, nextProps.selectedNetworkIds)
    );
  }
);

1.2. useMemo para enrichedLayouts

// Solo recalcular cuando cambien layouts o widgets
const enrichedLayouts = useMemo(() => {
  const widgetMap = new Map(widgets.map(w => [w.i, w]));
  const widgetTypeMap = new Map(widgetTypes.map(wt => [wt.id, wt]));

  const enrichLayout = (layout: GridLayout[]) =>
    layout.map(item => {
      const widget = widgetMap.get(item.i);
      const config = widget && widgetTypeMap.get(widget.type);
      return config ? { ...item, ...config.constraints } : item;
    });

  return {
    lg: enrichLayout(layouts.lg),
    md: enrichLayout(layouts.md),
    sm: enrichLayout(layouts.sm),
    xs: enrichLayout(layouts.xs),
  };
}, [layouts.lg, layouts.md, layouts.sm, layouts.xs, widgets]);

1.3. useCallback para handlers

const handleDuplicate = useCallback((widget: Widget) => {
  // Lógica de duplicación
}, [widgets, layouts]);

const handleRemove = useCallback((id: string) => {
  // Lógica de eliminación
}, [widgets, layouts]);

const handleLayoutChange = useCallback((currentLayout, allLayouts) => {
  setLayouts(allLayouts);
  if (isEditMode) setHasUnsavedChanges(true);
}, [isEditMode]);

1.4. Split de contexto

// Separar concerns para prevenir re-renders en cascada
<DashboardMetadataProvider value={{ dashboardId, name }}>
  <DashboardFiltersProvider value={filters}>
    <DashboardEditModeProvider value={{ isEditMode, hasChanges }}>
      {children}
    </DashboardEditModeProvider>
  </DashboardFiltersProvider>
</DashboardMetadataProvider>

// Widgets solo suscriben a filters, no a metadata ni editMode

🎯 Prioridad 2: Optimizar Reactividad a Filtros

2.1. Selector granular en context

// Widget solo re-renderiza si cambia dateRange
function MyWidget() {
  const dateRange = useDashboardFilters(f => f.dateRange);
  // No re-renderiza cuando cambian networks
}

2.2. Debouncing inteligente

import { useDebouncedCallback } from 'use-debounce';

const debouncedDateChange = useDebouncedCallback(
  (range: DateRange) => {
    updatePendingDateRange(range);
  },
  150 // 150ms - balance entre responsividad y performance
);

2.3. Memoización de props de widgets

const widgetProps = useMemo(() => ({
  dashboardId,
  dateRange,
  selectedNetworkIds,
  selectedNetworkGroupIds,
}), [
  dashboardId,
  dateRange?.from?.getTime(), // Comparar por timestamp
  dateRange?.to?.getTime(),
  JSON.stringify(selectedNetworkIds), // Comparar contenido
  JSON.stringify(selectedNetworkGroupIds),
]);

2.4. Prevent re-render del grid layout

// Memoizar callbacks pasados a ResponsiveGridLayout
const memoizedOnLayoutChange = useCallback((layout, allLayouts) => {
  // Solo actualizar si realmente cambió
  if (!areLayoutsEqual(allLayouts, prevLayouts)) {
    setLayouts(allLayouts);
  }
}, []);

🎯 Prioridad 3: Performance General

3.1. Remover todos los console.logs

// utils.ts
export const devLog = process.env.NODE_ENV === 'development'
  ? console.log.bind(console)
  : () => {};

// Uso
devLog('[normalizeLayouts] Input lg layouts:', layouts.lg);

3.2. Optimizar deep copies

// ANTES: Lento
setOriginalLayouts(JSON.parse(JSON.stringify(layouts)));

// DESPUÉS: 3-5x más rápido
setOriginalLayouts(structuredClone(layouts));

3.3. Lazy load de Framer Motion

// Solo cargar animaciones en edit mode
const MotionDiv = isEditMode
  ? motion.div
  : 'div' as any;

<MotionDiv animate={isEditMode ? animations : undefined}>
  {children}
</MotionDiv>

3.4. Memoizar cálculos costosos

// availableNetworks - solo recalcular si cambian dependencies
const availableNetworks = useMemo(() => {
  if (!selectedNetworkGroups?.length || selectedNetworkGroups.includes(0)) {
    return allNetworks;
  }
  return allNetworks.filter(n =>
    n.networkGroupId && selectedNetworkGroups.includes(n.networkGroupId)
  );
}, [allNetworks, selectedNetworkGroups]);

// formattedPresets - calcular una sola vez
const formattedPresets = useMemo(() =>
  dateRangePresets.map(preset => ({
    label: preset.label,
    value: preset.getValue(),
  })),
  [] // Sin dependencias - presets son estáticos
);

Métricas a Comparar

Métricas Primarias (Críticas)

Métrica Original (Esperado) Optimizado (Target) Mejora
Time to Interactive ~8s <2s 75%
Re-render Count (al cambiar filtros) 10-15 0-2 90% 🎯
Tiempo de respuesta a filtros ~500ms <100ms 80% 🚀
Freezing al cambiar filtros Frecuente Ninguno 100%

Métricas Secundarias (Importantes)

Métrica Original (Esperado) Optimizado (Target) Mejora
First Contentful Paint ~3s <1s 67%
Largest Contentful Paint ~5s <2.5s 50%
Total Blocking Time >500ms <200ms 60%
Memory Usage ~150MB ~80MB 47%
Bundle Size (initial) ~800KB ~300KB 63%

Cómo Medir

1. React DevTools Profiler

// Envolver componente con Profiler
import { Profiler } from 'react';

<Profiler id="Dashboard" onRender={(id, phase, duration) => {
  console.log(`${id} - ${phase}: ${duration}ms`);
}}>
  <DashboardClient />
</Profiler>

2. Performance API

const startTime = performance.now();
// ... render
const endTime = performance.now();
console.log(`Render time: ${endTime - startTime}ms`);

3. Chrome DevTools

  • Performance tab: Grabar durante cambio de filtros
  • Memory tab: Heap snapshot antes/después
  • Network tab: Requests durante carga
  • Coverage tab: Bundle size y code splitting

4. Lighthouse

npm run build
npm run start
# Abrir Chrome DevTools > Lighthouse > Run

Implementación Paso a Paso

Fase 1: Setup (1 hora)

  1. Crear estructura de carpetas
  2. Copiar DynamicDashboardClient.tsx → OptimizedDashboardClient.tsx
  3. Crear página POC con tabs
  4. Setup PerformanceMonitor

Fase 2: Context Optimization (2 horas)

  1. Crear OptimizedDashboardFiltersContext.tsx
  2. Implementar split de contextos
  3. Agregar selectores granulares
  4. Integrar debouncing

Fase 3: Component Memoization (3 horas)

  1. Crear MemoizedWidgetRenderer.tsx
  2. Implementar React.memo con comparador custom
  3. Agregar useCallback a todos los handlers
  4. Memoizar props de widgets

Fase 4: Layout Optimization (2 horas)

  1. Crear useOptimizedLayouts.ts
  2. Implementar Map lookups
  3. Agregar memoización profunda
  4. Optimizar cálculos de proporciones

Fase 5: General Performance (2 horas)

  1. Crear utils.ts con devLog
  2. Remover todos los console.logs
  3. Reemplazar JSON.parse/stringify con structuredClone
  4. Memoizar availableNetworks y formattedPresets
  5. Lazy load Framer Motion

Fase 6: Testing & Refinement (2 horas)

  1. Probar ambas versiones lado a lado
  2. Medir métricas con PerformanceMonitor
  3. Verificar reactividad a filtros
  4. Ajustar basado en resultados
  5. Documentar hallazgos

Total estimado: 12 horas (1.5 días de trabajo)


Criterios de Éxito

Must-Have (Obligatorios)

  • Reducción de al menos 70% en re-renders innecesarios
  • Tiempo de respuesta a filtros <150ms
  • Sin freezing perceptible al cambiar filtros
  • 100% de reactividad funcional mantenida
  • Time to Interactive <3s

Nice-to-Have (Deseables)

  • Reducción de 50%+ en memory usage
  • Bundle size <400KB inicial
  • First Contentful Paint <1.5s
  • Código más mantenible y documentado
  • Patrones reutilizables para otros dashboards

Próximos Pasos Post-POC

Si la POC es Exitosa (>70% mejora)

Fase 1: Aplicar al Dashboard Principal (1 semana)

  1. Migrar OptimizedDashboardClient.tsx → DynamicDashboardClient.tsx
  2. Actualizar DashboardFiltersContext con split y selectores
  3. Aplicar memoización a todos los 30+ widgets
  4. Testing exhaustivo en dev environment

Fase 2: Extender Optimizaciones (1 semana)

  1. Implementar lazy loading de widgets
  2. Agregar virtualización para dashboards con >12 widgets
  3. Setup Web Workers para cálculos pesados
  4. Implementar service worker para caching

Fase 3: Monitoreo y Refinamiento (ongoing)

  1. Agregar PerformanceMonitor en production (modo dev)
  2. Setup alerts para performance degradation
  3. A/B testing con usuarios reales
  4. Iterar basado en feedback

Fase 4: Documentación (2 días)

  1. Actualizar CLAUDE.md con patrones de optimización
  2. Crear guía de "Performance Best Practices"
  3. Documentar en changelog.MD
  4. Training para el equipo

Si la POC No Alcanza Targets (<50% mejora)

Plan B: Análisis Adicional

  1. Profiling más profundo con React DevTools
  2. Identificar cuellos de botella específicos
  3. Considerar refactorización arquitectural
  4. Evaluar alternativas (TanStack Virtual, Virtuoso, etc.)

Riesgos y Mitigaciones

Riesgo 1: Memoización Incorrecta

Impacto: Widgets no actualizan cuando deberían Mitigación:

  • Testing exhaustivo de reactividad
  • Agregar integration tests
  • Logging de re-renders en dev mode

Riesgo 2: Over-optimization

Impacto: Código complejo sin beneficio real Mitigación:

  • Medir antes y después de cada optimización
  • Solo aplicar si hay mejora >10%
  • Mantener simplicidad donde sea posible

Riesgo 3: Incompatibilidad con Backend

Impacto: Breaking changes en APIs Mitigación:

  • No modificar contratos de API
  • Mantener backward compatibility
  • Testing con datos reales del backend

Riesgo 4: Browser Compatibility

Impacto: structuredClone no disponible en navegadores viejos Mitigación:

  • Agregar polyfill o fallback a JSON.parse
  • Testing en Chrome, Firefox, Safari, Edge
  • Soporte para versiones recientes (últimos 2 años)

Recursos y Referencias

Documentación Oficial

Herramientas

Artículos Relevantes


Notas Adicionales

Consideraciones de Escalabilidad

  • POC con 6 widgets → Si exitoso, aplicar a 30+ widgets
  • Dashboard actual: <6 widgets típicamente
  • Plan futuro: Soporte para dashboards grandes (20+ widgets)
  • Virtualización será crítica para dashboards grandes

Mantenibilidad

  • Código optimizado debe ser fácil de entender
  • Agregar comentarios explicando optimizaciones
  • Documentar patrones para uso futuro
  • Training al equipo sobre performance patterns

Compatibilidad

  • Next.js 14+ (App Router)
  • React 18+ (para useMemo, useCallback mejorados)
  • TanStack Query v4+
  • Node 18+ (para structuredClone)

Changelog

2025-01-XX - Plan Inicial

  • Creación del plan de POC de optimización
  • Identificación de 7 problemas críticos de performance
  • Diseño de arquitectura optimizada con 3 niveles de contexto
  • Definición de métricas y criterios de éxito
  • Estimación de 12 horas de implementación

Apéndice: Análisis Técnico Detallado

A. Problemas Identificados con Líneas de Código

A.1. Re-renders Masivos (Líneas 971-1207)

Problema: Widgets no memoizados se re-renderizan en cada cambio de estado Evidencia:

{widgets.map((widget) => {
  const WidgetComponent = widgetComponents[widget.type];
  // Sin React.memo, TODOS los widgets se re-renderizan
  // cuando cambia CUALQUIER estado del padre
  return <div key={widget.i}>...</div>;
})}

A.2. Cálculos Costosos (Líneas 718-780)

Problema: enrichedLayouts se recalcula en CADA render Evidencia:

// Sin useMemo - se ejecuta en CADA render
const enrichedLayouts = {
  lg: (layouts.lg || []).map((layout) => { /* O(n) */ }),
  md: (layouts.md || []).map((layout) => { /* O(n) */ }),
  sm: (layouts.sm || []).map((layout) => { /* O(n) */ }),
  xs: (layouts.xs || []).map((layout) => { /* O(n) */ }),
};
// Total: O(4n) = O(n) pero ejecutado en cada render

A.3. Context Global (Líneas 783-788)

Problema: Un solo contexto para todo causa re-renders en cascada Evidencia:

<DashboardFiltersProvider
  initialFilters={initialFilters}
  dashboardId={dashboardId}
  onSaveFilters={handleSaveFilters}
>
  {/* TODOS los widgets re-renderizan cuando cambia CUALQUIER filtro */}
  <ResponsiveGridLayout>...</ResponsiveGridLayout>
</DashboardFiltersProvider>

A.4. Múltiples useEffect (Líneas 332-405)

Problema: Dependencias complejas causan ejecuciones inesperadas Evidencia:

useEffect(() => {
  // Se ejecuta demasiado frecuentemente
}, [backendDashboard?.selectedNetworks, isDashboardLoading, ...]);

B. Patrones de Optimización Aplicados

B.1. Memoización en Tres Niveles

  1. Componente: React.memo con comparador
  2. Props: useMemo para objetos complejos
  3. Callbacks: useCallback para funciones

B.2. Split de Contextos (Separation of Concerns)

DashboardMetadataContext  → Cambia raramente (dashboardId, name)
         ↓
DashboardFiltersContext   → Cambia frecuentemente (date, networks)
         ↓
DashboardEditModeContext  → No afecta a widgets (isEditMode, hasChanges)

B.3. Selectores Granulares

// Widget solo suscribe a dateRange
const dateRange = useDashboardFilters(f => f.dateRange);
// NO se re-renderiza cuando cambian networks

Fin del Plan - Listo para Implementación