skills/creating-dashboards/references/performance-optimization.md

18 KiB

Dashboard Performance Optimization

Table of Contents

Loading Strategies

Progressive Widget Loading

function ProgressiveDashboard({ widgets }) {
  const [loadPhase, setLoadPhase] = useState(0);
  const [loadedWidgets, setLoadedWidgets] = useState<Set<string>>(new Set());

  useEffect(() => {
    const loadingStrategy = [
      // Phase 1: Critical above-the-fold widgets
      widgets.filter(w => w.priority === 'critical'),
      // Phase 2: Visible widgets
      widgets.filter(w => w.priority === 'high'),
      // Phase 3: Below-the-fold widgets
      widgets.filter(w => w.priority === 'normal'),
      // Phase 4: Optional widgets
      widgets.filter(w => w.priority === 'low')
    ];

    loadingStrategy.forEach((phase, index) => {
      setTimeout(() => {
        setLoadPhase(index + 1);
        phase.forEach(widget => {
          setLoadedWidgets(prev => new Set(prev).add(widget.id));
        });
      }, index * 200); // Stagger loading
    });
  }, [widgets]);

  return (
    <div className="dashboard">
      {widgets.map(widget => (
        <div key={widget.id} className="widget-container">
          {loadedWidgets.has(widget.id) ? (
            <Widget {...widget} />
          ) : (
            <WidgetPlaceholder />
          )}
        </div>
      ))}
    </div>
  );
}

Intersection Observer Loading

function LazyWidget({ widget }) {
  const ref = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState(false);
  const [hasLoaded, setHasLoaded] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          if (!hasLoaded) {
            setHasLoaded(true);
          }
        } else {
          setIsVisible(false);
        }
      },
      {
        threshold: 0.1,
        rootMargin: '50px' // Start loading 50px before visible
      }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, [hasLoaded]);

  return (
    <div ref={ref} className="widget-wrapper">
      {hasLoaded ? (
        <Widget {...widget} isVisible={isVisible} />
      ) : (
        <WidgetSkeleton height={widget.height} />
      )}
    </div>
  );
}

Priority-Based Loading Queue

class WidgetLoader {
  private queue: PriorityQueue<Widget>;
  private loading = new Map<string, Promise<any>>();
  private maxConcurrent = 4;
  private currentLoading = 0;

  constructor() {
    this.queue = new PriorityQueue((a, b) => b.priority - a.priority);
  }

  async loadWidget(widget: Widget): Promise<any> {
    // Check if already loading
    if (this.loading.has(widget.id)) {
      return this.loading.get(widget.id);
    }

    // Add to queue
    this.queue.enqueue(widget);
    return this.processQueue();
  }

  private async processQueue() {
    while (this.currentLoading < this.maxConcurrent && !this.queue.isEmpty()) {
      const widget = this.queue.dequeue();
      if (!widget) break;

      this.currentLoading++;
      const loadPromise = this.fetchWidgetData(widget)
        .finally(() => {
          this.currentLoading--;
          this.loading.delete(widget.id);
          this.processQueue(); // Process next in queue
        });

      this.loading.set(widget.id, loadPromise);
    }
  }

  private async fetchWidgetData(widget: Widget) {
    const response = await fetch(`/api/widget/${widget.id}/data`);
    return response.json();
  }
}

// Usage
const widgetLoader = new WidgetLoader();

function PriorityLoadedWidget({ widget }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    widgetLoader.loadWidget(widget).then(setData);
  }, [widget]);

  return data ? <Widget data={data} /> : <Loading />;
}

Rendering Optimization

React.memo for Widget Optimization

const OptimizedWidget = React.memo(
  ({ id, data, filters }) => {
    console.log(`Rendering widget ${id}`);

    return (
      <div className="widget">
        <WidgetHeader id={id} />
        <WidgetContent data={data} />
        <WidgetFooter filters={filters} />
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Custom comparison function
    // Only re-render if data actually changed
    return (
      prevProps.id === nextProps.id &&
      JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) &&
      JSON.stringify(prevProps.filters) === JSON.stringify(nextProps.filters)
    );
  }
);

Virtualization for Large Widget Lists

import { VariableSizeGrid } from 'react-window';

function VirtualizedDashboard({ widgets, columns = 4 }) {
  const getItemSize = (index: number) => {
    const widget = widgets[index];
    return widget.height || 300; // Default height
  };

  const Cell = ({ columnIndex, rowIndex, style }) => {
    const index = rowIndex * columns + columnIndex;
    if (index >= widgets.length) return null;

    return (
      <div style={style}>
        <OptimizedWidget {...widgets[index]} />
      </div>
    );
  };

  return (
    <VariableSizeGrid
      columnCount={columns}
      columnWidth={index => 300} // Fixed column width
      height={window.innerHeight}
      rowCount={Math.ceil(widgets.length / columns)}
      rowHeight={getItemSize}
      width={window.innerWidth}
      overscanRowCount={2} // Render 2 extra rows for smooth scrolling
    >
      {Cell}
    </VariableSizeGrid>
  );
}

Batched State Updates

function useBatchedUpdates() {
  const [updates, setUpdates] = useState<Update[]>([]);
  const pendingUpdates = useRef<Update[]>([]);
  const rafId = useRef<number>();

  const scheduleUpdate = useCallback((update: Update) => {
    pendingUpdates.current.push(update);

    // Cancel previous frame
    if (rafId.current) {
      cancelAnimationFrame(rafId.current);
    }

    // Schedule batch update
    rafId.current = requestAnimationFrame(() => {
      setUpdates(current => [...current, ...pendingUpdates.current]);
      pendingUpdates.current = [];
    });
  }, []);

  useEffect(() => {
    return () => {
      if (rafId.current) {
        cancelAnimationFrame(rafId.current);
      }
    };
  }, []);

  return { updates, scheduleUpdate };
}

Data Management

Efficient Data Structures

class DashboardDataStore {
  private widgetData = new Map<string, any>();
  private subscriptions = new Map<string, Set<Function>>();
  private cache = new LRUCache<string, any>(100); // Max 100 entries

  setWidgetData(widgetId: string, data: any) {
    // Store in map
    this.widgetData.set(widgetId, data);

    // Update cache
    this.cache.set(widgetId, {
      data,
      timestamp: Date.now()
    });

    // Notify subscribers
    this.notifySubscribers(widgetId, data);
  }

  getWidgetData(widgetId: string): any {
    // Try cache first
    const cached = this.cache.get(widgetId);
    if (cached && Date.now() - cached.timestamp < 60000) {
      return cached.data;
    }

    // Return from store
    return this.widgetData.get(widgetId);
  }

  subscribe(widgetId: string, callback: Function) {
    if (!this.subscriptions.has(widgetId)) {
      this.subscriptions.set(widgetId, new Set());
    }
    this.subscriptions.get(widgetId)!.add(callback);

    // Return unsubscribe function
    return () => {
      const subs = this.subscriptions.get(widgetId);
      if (subs) {
        subs.delete(callback);
      }
    };
  }

  private notifySubscribers(widgetId: string, data: any) {
    const subs = this.subscriptions.get(widgetId);
    if (subs) {
      subs.forEach(callback => callback(data));
    }
  }

  clearCache() {
    this.cache.clear();
  }

  clearWidget(widgetId: string) {
    this.widgetData.delete(widgetId);
    this.cache.delete(widgetId);
    this.subscriptions.delete(widgetId);
  }
}

Data Aggregation and Transformation

function useAggregatedData(widgets: Widget[]) {
  const [aggregated, setAggregated] = useState({});

  useEffect(() => {
    const worker = new Worker('/workers/data-aggregator.js');

    worker.postMessage({
      type: 'AGGREGATE',
      widgets: widgets.map(w => ({ id: w.id, type: w.type }))
    });

    worker.onmessage = (event) => {
      if (event.data.type === 'AGGREGATED') {
        setAggregated(event.data.result);
      }
    };

    return () => worker.terminate();
  }, [widgets]);

  return aggregated;
}

// Web Worker: data-aggregator.js
self.onmessage = function(event) {
  if (event.data.type === 'AGGREGATE') {
    const result = {};

    event.data.widgets.forEach(widget => {
      // Perform heavy computations
      result[widget.id] = performAggregation(widget);
    });

    self.postMessage({
      type: 'AGGREGATED',
      result
    });
  }
};

Memory Management

Widget Cleanup

function Widget({ id, data }) {
  useEffect(() => {
    // Setup
    const charts = initializeCharts(data);
    const subscriptions = setupSubscriptions(id);

    // Cleanup
    return () => {
      // Dispose of chart instances
      charts.forEach(chart => chart.destroy());

      // Cancel subscriptions
      subscriptions.forEach(sub => sub.unsubscribe());

      // Clear timers
      clearInterval(refreshTimer);

      // Clear cached data
      widgetCache.delete(id);
    };
  }, [id]);

  return <div id={`widget-${id}`}>...</div>;
}

Memory Leak Prevention

function useSafeAsync() {
  const mounted = useRef(true);
  const abortControllers = useRef<Set<AbortController>>(new Set());

  useEffect(() => {
    return () => {
      mounted.current = false;
      // Abort all pending requests
      abortControllers.current.forEach(controller => controller.abort());
    };
  }, []);

  const safeAsync = useCallback(async (asyncFunc: () => Promise<any>) => {
    const controller = new AbortController();
    abortControllers.current.add(controller);

    try {
      const result = await asyncFunc();

      if (mounted.current) {
        return result;
      }
    } catch (error) {
      if (error.name !== 'AbortError') {
        throw error;
      }
    } finally {
      abortControllers.current.delete(controller);
    }
  }, []);

  return safeAsync;
}

Resource Pooling

class ChartPool {
  private available: Chart[] = [];
  private inUse = new Map<string, Chart>();
  private maxSize = 20;

  acquire(widgetId: string, config: ChartConfig): Chart {
    let chart = this.available.pop();

    if (!chart) {
      chart = new Chart(config);
    } else {
      chart.update(config);
    }

    this.inUse.set(widgetId, chart);
    return chart;
  }

  release(widgetId: string) {
    const chart = this.inUse.get(widgetId);

    if (chart) {
      this.inUse.delete(widgetId);

      if (this.available.length < this.maxSize) {
        chart.clear();
        this.available.push(chart);
      } else {
        chart.destroy();
      }
    }
  }

  clear() {
    this.inUse.forEach(chart => chart.destroy());
    this.available.forEach(chart => chart.destroy());
    this.inUse.clear();
    this.available = [];
  }
}

const chartPool = new ChartPool();

function PooledChartWidget({ id, config }) {
  useEffect(() => {
    const chart = chartPool.acquire(id, config);

    return () => {
      chartPool.release(id);
    };
  }, [id, config]);

  return <div id={`chart-${id}`} />;
}

Network Optimization

Request Batching

class RequestBatcher {
  private batch: Map<string, Promise<any>> = new Map();
  private timer: NodeJS.Timeout | null = null;
  private delay = 50; // Batch window in ms

  async fetch(endpoint: string, params: any): Promise<any> {
    const key = `${endpoint}:${JSON.stringify(params)}`;

    // Check if request already in batch
    if (this.batch.has(key)) {
      return this.batch.get(key);
    }

    // Create promise for this request
    const promise = new Promise((resolve, reject) => {
      // Add to batch
      this.addToBatch(key, { endpoint, params, resolve, reject });
    });

    this.batch.set(key, promise);
    return promise;
  }

  private addToBatch(key: string, request: BatchRequest) {
    if (!this.timer) {
      this.timer = setTimeout(() => this.executeBatch(), this.delay);
    }
  }

  private async executeBatch() {
    const requests = Array.from(this.batch.entries());
    this.batch.clear();
    this.timer = null;

    try {
      // Send all requests in single call
      const response = await fetch('/api/batch', {
        method: 'POST',
        body: JSON.stringify(requests.map(([key, _]) => key))
      });

      const results = await response.json();

      // Resolve individual promises
      requests.forEach(([key, promise], index) => {
        promise.resolve(results[index]);
      });
    } catch (error) {
      requests.forEach(([key, promise]) => {
        promise.reject(error);
      });
    }
  }
}

Response Compression

function useCompressedData(endpoint: string) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(endpoint, {
      headers: {
        'Accept-Encoding': 'gzip, deflate, br'
      }
    })
    .then(response => {
      // Check if response is compressed
      const encoding = response.headers.get('content-encoding');
      console.log(`Response compressed with: ${encoding}`);

      return response.json();
    })
    .then(setData);
  }, [endpoint]);

  return data;
}

Prefetching Strategies

function usePrefetch() {
  const prefetchCache = useRef(new Map());

  const prefetch = useCallback(async (widgets: Widget[]) => {
    // Identify widgets likely to be needed
    const toPrefetch = widgets.filter(w =>
      w.priority === 'high' && !prefetchCache.current.has(w.id)
    );

    // Prefetch in background
    toPrefetch.forEach(async widget => {
      const data = await fetch(`/api/widget/${widget.id}/data`, {
        priority: 'low' // Low priority fetch
      });

      prefetchCache.current.set(widget.id, await data.json());
    });
  }, []);

  const getPrefetched = useCallback((widgetId: string) => {
    const data = prefetchCache.current.get(widgetId);
    if (data) {
      prefetchCache.current.delete(widgetId); // Use once
    }
    return data;
  }, []);

  return { prefetch, getPrefetched };
}

Monitoring & Profiling

Performance Metrics Collection

class DashboardMetrics {
  private metrics = {
    widgetLoadTimes: new Map<string, number>(),
    renderTimes: new Map<string, number>(),
    dataFetchTimes: new Map<string, number>(),
    errors: []
  };

  startTimer(category: string, id: string) {
    const key = `${category}:${id}`;
    performance.mark(`${key}:start`);
  }

  endTimer(category: string, id: string) {
    const key = `${category}:${id}`;
    performance.mark(`${key}:end`);

    performance.measure(
      key,
      `${key}:start`,
      `${key}:end`
    );

    const measure = performance.getEntriesByName(key)[0];
    this.metrics[category].set(id, measure.duration);
  }

  getReport() {
    const avgLoadTime = this.calculateAverage(this.metrics.widgetLoadTimes);
    const avgRenderTime = this.calculateAverage(this.metrics.renderTimes);
    const avgFetchTime = this.calculateAverage(this.metrics.dataFetchTimes);

    return {
      averageLoadTime: avgLoadTime,
      averageRenderTime: avgRenderTime,
      averageFetchTime: avgFetchTime,
      totalErrors: this.metrics.errors.length,
      slowestWidget: this.findSlowest(this.metrics.widgetLoadTimes),
      performance: this.calculatePerformanceScore()
    };
  }

  private calculateAverage(map: Map<string, number>): number {
    const values = Array.from(map.values());
    return values.reduce((a, b) => a + b, 0) / values.length;
  }

  private findSlowest(map: Map<string, number>): string {
    let slowest = '';
    let maxTime = 0;

    map.forEach((time, id) => {
      if (time > maxTime) {
        maxTime = time;
        slowest = id;
      }
    });

    return slowest;
  }

  private calculatePerformanceScore(): number {
    const avgLoad = this.calculateAverage(this.metrics.widgetLoadTimes);

    if (avgLoad < 100) return 100;
    if (avgLoad < 300) return 90;
    if (avgLoad < 500) return 70;
    if (avgLoad < 1000) return 50;
    return 30;
  }
}

// Usage
const metrics = new DashboardMetrics();

function MonitoredWidget({ id, ...props }) {
  useEffect(() => {
    metrics.startTimer('widgetLoadTimes', id);

    return () => {
      metrics.endTimer('widgetLoadTimes', id);
    };
  }, [id]);

  return <Widget id={id} {...props} />;
}

Real User Monitoring (RUM)

function useRUM() {
  useEffect(() => {
    // Core Web Vitals
    if ('PerformanceObserver' in window) {
      // Largest Contentful Paint
      const lcpObserver = new PerformanceObserver((entries) => {
        const lastEntry = entries.getEntries().pop();
        console.log('LCP:', lastEntry?.startTime);

        // Send to analytics
        analytics.track('dashboard_lcp', {
          value: lastEntry?.startTime,
          url: window.location.href
        });
      });
      lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

      // First Input Delay
      const fidObserver = new PerformanceObserver((entries) => {
        entries.getEntries().forEach(entry => {
          console.log('FID:', entry.processingStart - entry.startTime);
        });
      });
      fidObserver.observe({ entryTypes: ['first-input'] });

      // Cumulative Layout Shift
      let clsValue = 0;
      const clsObserver = new PerformanceObserver((entries) => {
        entries.getEntries().forEach(entry => {
          if (!entry.hadRecentInput) {
            clsValue += entry.value;
            console.log('CLS:', clsValue);
          }
        });
      });
      clsObserver.observe({ entryTypes: ['layout-shift'] });

      return () => {
        lcpObserver.disconnect();
        fidObserver.disconnect();
        clsObserver.disconnect();
      };
    }
  }, []);
}