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
- Re-renders masivos en cascada - Cada widget se re-renderiza cuando cambia CUALQUIER estado del padre
- Cálculos costosos de layout - Se ejecutan en cada render (operaciones O(n²))
- Sin memoización - Ningún componente usa React.memo
- Todos los widgets se cargan al inicio - No hay lazy loading ni code splitting
- 30+ console.logs en producción - Fugas de memoria y overhead
- Context global - Todos los widgets se re-renderizan cuando cambian los filtros
- 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:
-
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)
-
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);
- Debouncing inteligente - 150ms para date picker (mantiene UX fluida)
D. Hook de Layout Optimizado
Archivo: _poc/useOptimizedLayouts.ts
Optimizaciones:
- 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]));
- 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]);
- 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:
-
RealTimeUsers (SplashWidgetType.RealTimeUsers)
- Actualización frecuente
- Datos en tiempo real
- KPI simple
-
ConnectedClientDetails (SplashWidgetType.ConnectedClientDetails)
- Tabla con muchos datos
- Paginación
- Alto consumo de memoria
-
VisitsHistorical (SplashWidgetType.VisitsHistorical)
- Gráfico complejo (recharts)
- Muchos data points
- Sensible a cambios de fecha
-
UniqueUsers (SplashWidgetType.UniqueUsers)
- KPI simple
- Datos agregados
- Rápido de renderizar
-
NewVsReturning (SplashWidgetType.NewVsReturning)
- Pie chart
- Animaciones
- Mediana complejidad
-
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)
- ✅ Crear estructura de carpetas
- ✅ Copiar DynamicDashboardClient.tsx → OptimizedDashboardClient.tsx
- ✅ Crear página POC con tabs
- ✅ Setup PerformanceMonitor
Fase 2: Context Optimization (2 horas)
- ✅ Crear OptimizedDashboardFiltersContext.tsx
- ✅ Implementar split de contextos
- ✅ Agregar selectores granulares
- ✅ Integrar debouncing
Fase 3: Component Memoization (3 horas)
- ✅ Crear MemoizedWidgetRenderer.tsx
- ✅ Implementar React.memo con comparador custom
- ✅ Agregar useCallback a todos los handlers
- ✅ Memoizar props de widgets
Fase 4: Layout Optimization (2 horas)
- ✅ Crear useOptimizedLayouts.ts
- ✅ Implementar Map lookups
- ✅ Agregar memoización profunda
- ✅ Optimizar cálculos de proporciones
Fase 5: General Performance (2 horas)
- ✅ Crear utils.ts con devLog
- ✅ Remover todos los console.logs
- ✅ Reemplazar JSON.parse/stringify con structuredClone
- ✅ Memoizar availableNetworks y formattedPresets
- ✅ Lazy load Framer Motion
Fase 6: Testing & Refinement (2 horas)
- ✅ Probar ambas versiones lado a lado
- ✅ Medir métricas con PerformanceMonitor
- ✅ Verificar reactividad a filtros
- ✅ Ajustar basado en resultados
- ✅ 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)
- Migrar OptimizedDashboardClient.tsx → DynamicDashboardClient.tsx
- Actualizar DashboardFiltersContext con split y selectores
- Aplicar memoización a todos los 30+ widgets
- Testing exhaustivo en dev environment
Fase 2: Extender Optimizaciones (1 semana)
- Implementar lazy loading de widgets
- Agregar virtualización para dashboards con >12 widgets
- Setup Web Workers para cálculos pesados
- Implementar service worker para caching
Fase 3: Monitoreo y Refinamiento (ongoing)
- Agregar PerformanceMonitor en production (modo dev)
- Setup alerts para performance degradation
- A/B testing con usuarios reales
- Iterar basado en feedback
Fase 4: Documentación (2 días)
- Actualizar CLAUDE.md con patrones de optimización
- Crear guía de "Performance Best Practices"
- Documentar en changelog.MD
- Training para el equipo
Si la POC No Alcanza Targets (<50% mejora)
Plan B: Análisis Adicional
- Profiling más profundo con React DevTools
- Identificar cuellos de botella específicos
- Considerar refactorización arquitectural
- 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
- Componente: React.memo con comparador
- Props: useMemo para objetos complejos
- 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 ✅