Skip to content

⚡ Task 10.2: Performance Optimization - Sub-Second Responsiveness #85

@Raafay-Qureshi

Description

@Raafay-Qureshi

⚡ Task 10.2: Performance Optimization - Sub-Second Responsiveness

Optimize frontend performance to ensure instant responsiveness and smooth 60fps animations.

📝 Description

Implement performance optimizations across the frontend to achieve professional-grade responsiveness. This includes code splitting, lazy loading components, memoization, virtualizing long lists, optimizing images, reducing bundle size, and ensuring all interactions feel instant. The goal is butter-smooth performance that makes judges say "wow, this is fast."

🎯 Acceptance Criteria

  • Bundle size < 500KB gzipped
  • Initial load < 2 seconds
  • Time to interactive < 3 seconds
  • All animations 60fps
  • No layout shifts (CLS = 0)
  • Lighthouse score > 90
  • React DevTools shows no unnecessary re-renders
  • Memory usage stable (no leaks)
  • Code splitting implemented
  • Lazy loading for heavy components

🛠️ Implementation

Code Splitting and Lazy Loading

Update src/App.js:

import React, { Suspense, lazy } from 'react';
import { WebSocketProvider } from './services/websocket';
import LoadingSpinner from './components/Shared/LoadingSpinner';
import './App.css';

// Lazy load heavy components
const Dashboard = lazy(() => import('./components/Dashboard'));

function App() {
  return (
    <WebSocketProvider url={process.env.REACT_APP_API_URL?.replace('/api', '')}>
      <div className="App">
        <header className="app-header">
          <h1>🚨 RapidResponse AI</h1>
          <p className="subtitle">Emergency Response Intelligence System</p>
          <div className="disclaimer">
            ⚠️ SIMULATION MODE - For Demonstration Only
          </div>
        </header>
        
        <Suspense fallback={
          <div className="app-loading">
            <LoadingSpinner size="large" message="Loading Emergency Operations Center..." />
          </div>
        }>
          <Dashboard />
        </Suspense>
      </div>
    </WebSocketProvider>
  );
}

export default App;

React.memo for Components

Optimize heavy components with memoization:

Update src/components/Map/MapView.js:

import React, { useEffect, useRef, useState, memo } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import DangerZoneLayer from './DangerZoneLayer';
import EvacuationRoutes from './EvacuationRoutes';
import Markers from './Markers';
import './MapView.css';

mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN;

const MapView = memo(function MapView({ disaster, plan }) {
  // ... existing implementation ...
  
  return (
    <div className="map-view">
      {/* ... existing JSX ... */}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison function
  return (
    prevProps.disaster?.disaster_id === nextProps.disaster?.disaster_id &&
    prevProps.plan?.generated_at === nextProps.plan?.generated_at
  );
});

export default MapView;

useMemo for Expensive Calculations

Update src/components/EmergencyPlan/PopulationImpact.js:

import React, { useMemo } from 'react';
import './PopulationImpact.css';

function PopulationImpact({ populationImpact, affectedAreas }) {
  // Memoize expensive calculations
  const languageData = useMemo(() => {
    if (!populationImpact?.languages) return [];
    
    const totalLangSpeakers = Object.values(populationImpact.languages)
      .reduce((sum, count) => sum + count, 0);
    
    return Object.entries(populationImpact.languages)
      .map(([lang, count]) => ({
        language: lang,
        count: count,
        percentage: totalLangSpeakers > 0 
          ? (count / totalLangSpeakers * 100).toFixed(1) 
          : 0,
      }))
      .sort((a, b) => b.count - a.count);
  }, [populationImpact?.languages]);
  
  const vulnerablePercentage = useMemo(() => {
    if (!populationImpact) return 0;
    
    const { elderly = 0, children = 0, disabled = 0 } = populationImpact.vulnerable_population || {};
    const totalVulnerable = elderly + children + disabled;
    const total = populationImpact.total_affected || 0;
    
    return total > 0 ? ((totalVulnerable / total) * 100).toFixed(1) : 0;
  }, [populationImpact]);
  
  // ... rest of component
}

export default React.memo(PopulationImpact);

useCallback for Event Handlers

Update src/hooks/useDisaster.js:

import { useState, useCallback, useEffect, useMemo } from 'react';
import { disasterAPI } from '../services/api';
import useWebSocket from './useWebSocket';

function useDisaster() {
  const [disaster, setDisaster] = useState(null);
  const [plan, setPlan] = useState(null);
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState(null);
  const [statusMessage, setStatusMessage] = useState('');

  const { connected, on, subscribeToDisaster } = useWebSocket();

  // Memoize status values
  const status = useMemo(() => ({
    isProcessing: loading,
    hasActivePlan: !!plan,
    hasError: !!error,
  }), [loading, plan, error]);

  const triggerDisaster = useCallback(async (disasterData) => {
    // ... existing implementation (already uses useCallback)
  }, [subscribeToDisaster]);

  const clearDisaster = useCallback(() => {
    // ... existing implementation (already uses useCallback)
  }, []);

  return {
    disaster,
    plan,
    loading,
    progress,
    error,
    statusMessage,
    triggerDisaster,
    clearDisaster,
    ...status,
  };
}

export default useDisaster;

Image Optimization

Create utility for lazy image loading:

src/utils/lazyImage.js:

import { useEffect, useRef, useState } from 'react';

export function useLazyImage(src) {
  const [imageSrc, setImageSrc] = useState(null);
  const [imageRef, setImageRef] = useState(null);

  useEffect(() => {
    let observer;
    let didCancel = false;

    if (imageRef && imageSrc !== src) {
      if (IntersectionObserver) {
        observer = new IntersectionObserver(
          entries => {
            entries.forEach(entry => {
              if (
                !didCancel &&
                (entry.intersectionRatio > 0 || entry.isIntersecting)
              ) {
                setImageSrc(src);
                observer.unobserve(imageRef);
              }
            });
          },
          {
            threshold: 0.01,
            rootMargin: '75%',
          }
        );
        observer.observe(imageRef);
      } else {
        // Fallback for browsers without IntersectionObserver
        setImageSrc(src);
      }
    }
    
    return () => {
      didCancel = true;
      if (observer && observer.unobserve && imageRef) {
        observer.unobserve(imageRef);
      }
    };
  }, [src, imageSrc, imageRef]);

  return [setImageRef, imageSrc];
}

Bundle Size Optimization

Update package.json with tree-shaking optimized imports:

{
  "scripts": {
    "analyze": "source-map-explorer 'build/static/js/*.js'",
    "build": "GENERATE_SOURCEMAP=false react-scripts build"
  },
  "devDependencies": {
    "source-map-explorer": "^2.5.3"
  }
}

Analyze bundle:

npm run build
npm run analyze

Debounce Expensive Operations

Create src/utils/debounce.js:

export function debounce(func, wait) {
  let timeout;
  
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Usage example for search or filter operations
export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

Virtual Scrolling for Long Lists

If you have long lists (like many facilities), use virtual scrolling:

npm install react-window

Example usage in src/components/Map/Markers.js:

import { FixedSizeList } from 'react-window';

// For rendering many markers in a list view (if you add this feature)
function MarkersList({ markers }) {
  const Row = ({ index, style }) => (
    <div style={style} className="marker-row">
      {markers[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      itemCount={markers.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Optimize Re-renders with React DevTools

Add React DevTools Profiler in development:

Update src/index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

if (process.env.NODE_ENV === 'development') {
  // Enable React DevTools profiler
  root.render(
    <React.StrictMode>
      <React.Profiler id="App" onRender={(id, phase, actualDuration) => {
        console.log(`${id} (${phase}) took ${actualDuration}ms`);
      }}>
        <App />
      </React.Profiler>
    </React.StrictMode>
  );
} else {
  root.render(<App />);
}

Service Worker for Caching (Optional)

Enable service worker for faster repeat loads:

Update src/index.js:

import * as serviceWorkerRegistration from './serviceWorkerRegistration';

// Register service worker
serviceWorkerRegistration.register({
  onUpdate: registration => {
    console.log('New version available! Please refresh.');
  },
  onSuccess: registration => {
    console.log('Content cached for offline use.');
  },
});

Create src/serviceWorkerRegistration.js:

// Standard service worker registration
// Copy from Create React App template or implement basic caching
export function register(config) {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
      navigator.serviceWorker.register(swUrl)
        .then(registration => {
          if (config && config.onSuccess) {
            config.onSuccess(registration);
          }
        })
        .catch(error => {
          console.error('Service worker registration failed:', error);
        });
    });
  }
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(registration => {
      registration.unregister();
    });
  }
}

🧪 Performance Testing

Run Lighthouse Audit

# Build production version
npm run build

# Serve it
npx serve -s build

# Open Chrome DevTools > Lighthouse
# Run audit for Performance, Accessibility, Best Practices, SEO
# Target: All scores > 90

Check Bundle Size

npm run build

# Check sizes
ls -lh build/static/js/*.js
ls -lh build/static/css/*.css

# Should see:
# - Main JS: < 200KB gzipped
# - Vendor JS: < 300KB gzipped
# - CSS: < 50KB gzipped

Test Memory Leaks

// In DevTools Console, run this test:
let memoryBefore = performance.memory.usedJSHeapSize;
console.log('Memory before:', memoryBefore);

// Trigger disaster 5 times, wait for completion each time
// Then run:
let memoryAfter = performance.memory.usedJSHeapSize;
console.log('Memory after:', memoryAfter);
console.log('Increase:', ((memoryAfter - memoryBefore) / 1024 / 1024).toFixed(2), 'MB');

// Memory increase should be < 10MB

Test Frame Rate

// Add FPS counter (development only)
let lastTime = performance.now();
let frames = 0;

function measureFPS() {
  frames++;
  const now = performance.now();
  
  if (now >= lastTime + 1000) {
    const fps = Math.round((frames * 1000) / (now - lastTime));
    console.log('FPS:', fps);
    frames = 0;
    lastTime = now;
  }
  
  requestAnimationFrame(measureFPS);
}

measureFPS();
// Should consistently show 60 FPS during animations

📋 Performance Checklist

  • Lighthouse Performance score > 90
  • First Contentful Paint < 1.5s
  • Time to Interactive < 3s
  • Largest Contentful Paint < 2.5s
  • Cumulative Layout Shift < 0.1
  • Total bundle size < 500KB gzipped
  • No memory leaks (tested 10+ cycles)
  • All animations 60fps
  • React DevTools shows minimal re-renders
  • Images lazy-loaded

⏱️ Estimated Time

75 minutes

🔗 Related Documentation

React Performance: https://react.dev/learn/render-and-commit
Web Vitals: https://web.dev/vitals/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions