# 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:** ```typescript - 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:** ```typescript // 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:** ```typescript export const MemoizedWidgetRenderer = React.memo( 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 ; }, (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: ```typescript // Hook con selector opcional function useDashboardFilters(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); ``` 3. **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)): ```typescript const widgetMap = new Map(widgets.map(w => [w.i, w])); const widgetTypeMap = new Map(widgetTypes.map(wt => [wt.id, wt])); ``` 2. **Memoización profunda** - Solo recalcula cuando cambian layouts o widgets: ```typescript 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]); ``` 3. **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:** ```typescript {({ metrics }) => (
)}
``` --- #### F. Utilidades de Desarrollo **Archivo:** `_poc/utils.ts` **Helpers:** ```typescript // 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(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(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 ```typescript 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 ```typescript // 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 ```typescript 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 ```typescript // Separar concerns para prevenir re-renders en cascada {children} // Widgets solo suscriben a filters, no a metadata ni editMode ``` --- ### 🎯 Prioridad 2: Optimizar Reactividad a Filtros #### 2.1. Selector granular en context ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // Solo cargar animaciones en edit mode const MotionDiv = isEditMode ? motion.div : 'div' as any; {children} ``` #### 3.4. Memoizar cálculos costosos ```typescript // 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 ```typescript // Envolver componente con Profiler import { Profiler } from 'react'; { console.log(`${id} - ${phase}: ${duration}ms`); }}> ``` #### 2. Performance API ```typescript 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 ```bash 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 - [React.memo](https://react.dev/reference/react/memo) - [useMemo](https://react.dev/reference/react/useMemo) - [useCallback](https://react.dev/reference/react/useCallback) - [Profiler API](https://react.dev/reference/react/Profiler) ### Herramientas - [React DevTools Profiler](https://react.dev/learn/react-developer-tools) - [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) - [Lighthouse](https://developers.google.com/web/tools/lighthouse) - [Bundle Analyzer](https://www.npmjs.com/package/@next/bundle-analyzer) ### Artículos Relevantes - [Optimizing React Performance](https://react.dev/learn/render-and-commit) - [Before You memo()](https://overreacted.io/before-you-memo/) - [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/) --- ## 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:** ```typescript {widgets.map((widget) => { const WidgetComponent = widgetComponents[widget.type]; // Sin React.memo, TODOS los widgets se re-renderizan // cuando cambia CUALQUIER estado del padre return
...
; })} ``` #### A.2. Cálculos Costosos (Líneas 718-780) **Problema:** enrichedLayouts se recalcula en CADA render **Evidencia:** ```typescript // 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:** ```typescript {/* TODOS los widgets re-renderizan cuando cambia CUALQUIER filtro */} ... ``` #### A.4. Múltiples useEffect (Líneas 332-405) **Problema:** Dependencias complejas causan ejecuciones inesperadas **Evidencia:** ```typescript 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 ```typescript // 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** ✅