Performance Optimization Guide ⚡

Accelerate your reptile care app with advanced performance techniques. A comprehensive guide to making CrittrHavens blazing fast.

Abstract speed and performance visualization with colorful motion blur

Performance Philosophy

Speed is a feature, not a luxury.

Core Principles

Performance Strategy:

  • Measure First, Optimize Second: Data-driven performance improvements
  • Progressive Enhancement: Fast initial load, enhanced features later
  • Perceived Performance: Make it feel fast, not just be fast
  • Mobile-First Performance: Optimize for constrained devices
  • Continuous Monitoring: Track performance metrics over time

Performance Budget

Target Metrics:

// Performance goals
const performanceBudget = {
  // Core Web Vitals
  LCP: 2500,    // Largest Contentful Paint < 2.5s
  FID: 100,     // First Input Delay < 100ms
  CLS: 0.1,     // Cumulative Layout Shift < 0.1
  
  // Additional metrics
  TTI: 3800,    // Time to Interactive < 3.8s
  FCP: 1800,    // First Contentful Paint < 1.8s
  
  // Bundle sizes
  mainBundle: 200,   // Main JS bundle < 200KB
  cssBundle: 50,     // CSS bundle < 50KB
  totalSize: 500,    // Total resources < 500KB
};

Code Splitting and Lazy Loading

Load only what you need, when you need it.

Route-Based Code Splitting

Lazy Loading Routes:

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';

// Lazy load all route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const CrittrDetails = lazy(() => import('./pages/CrittrDetails'));
const HabitatDetails = lazy(() => import('./pages/HabitatDetails'));
const Reports = lazy(() => import('./pages/Reports'));

// Heavy features loaded on demand
const VetReport = lazy(() => 
  import(/* webpackChunkName: "vet-report" */ './pages/VetReport')
);

function App() {
  return (
    <Suspense fallback={<PageLoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/crittrs/:id" element={<CrittrDetails />} />
        <Route path="/habitats/:id" element={<HabitatDetails />} />
        <Route path="/reports" element={<Reports />} />
        <Route path="/vet-report" element={<VetReport />} />
      </Routes>
    </Suspense>
  );
}

Component-Level Code Splitting

Lazy Loading Heavy Components:

// Split heavy components
const ChartComponent = lazy(() => import('./components/ChartComponent'));
const ImageEditor = lazy(() => import('./components/ImageEditor'));
const PdfGenerator = lazy(() => import('./components/PdfGenerator'));

// Load on interaction
function ReportPage() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <Button onClick={() => setShowChart(true)}>
        Show Growth Chart
      </Button>
      
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <ChartComponent data={data} />
        </Suspense>
      )}
    
  );
}

Dynamic Imports

Conditional Module Loading:

// Load modules based on user actions
async function exportData(format: string) {
  switch (format) {
    case 'pdf':
      const { generatePDF } = await import('./utils/pdf-generator');
      return generatePDF(data);
      
    case 'excel':
      const { generateExcel } = await import('./utils/excel-generator');
      return generateExcel(data);
      
    case 'csv':
      const { generateCSV } = await import('./utils/csv-generator');
      return generateCSV(data);
  }
}

// Load polyfills only when needed
async function loadPolyfills() {
  if (!window.IntersectionObserver) {
    await import('intersection-observer');
  }
  
  if (!window.ResizeObserver) {
    await import('resize-observer-polyfill');
  }
}

Image Optimization Strategies

Serving the right image at the right time.

Lazy Loading Images

Intersection Observer Pattern:

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

export function LazyImage({ src, alt, placeholder, className }) {
  const [imageSrc, setImageSrc] = useState(placeholder);
  const [imageRef, setImageRef] = useState<HTMLImageElement>();
  const observerRef = useRef<IntersectionObserver>();

  useEffect(() => {
    if (!imageRef) return;

    observerRef.current = new IntersectionObserver(
      entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            // Load the real image
            setImageSrc(src);
            
            // Stop observing
            observerRef.current?.unobserve(entry.target);
          }
        });
      },
      {
        rootMargin: '50px', // Start loading 50px before visible
      }
    );

    observerRef.current.observe(imageRef);

    return () => {
      observerRef.current?.disconnect();
    };
  }, [imageRef, src]);

  return (
    <img
      ref={setImageRef}
      src={imageSrc}
      alt={alt}
      className={className}
      loading="lazy" // Native lazy loading fallback
    />
  );
}

Responsive Images

Serving Optimized Sizes:

export function ResponsiveImage({ src, alt, sizes }) {
  // Generate srcset for different resolutions
  const generateSrcSet = (baseUrl: string) => {
    const widths = [320, 640, 768, 1024, 1280, 1920];
    return widths
      .map(w => `${baseUrl}?w=${w} ${w}w`)
      .join(', ');
  };

  return (
    <picture>
      {/* WebP for modern browsers */}
      <source
        type="image/webp"
        srcSet={generateSrcSet(src.replace(/\.[^.]+$/, '.webp'))}
        sizes={sizes}
      />
      
      {/* JPEG fallback */}
      <source
        type="image/jpeg"
        srcSet={generateSrcSet(src)}
        sizes={sizes}
      />
      
      <img
        src={src}
        alt={alt}
        loading="lazy"
        decoding="async"
      />
    </picture>
  );
}

Image Preprocessing

Supabase Storage Transformation:

// Generate optimized image URLs
export function getOptimizedImageUrl(
  path: string,
  options: ImageOptions = {}
) {
  const {
    width = 800,
    height,
    quality = 80,
    format = 'webp'
  } = options;

  const params = new URLSearchParams({
    width: width.toString(),
    quality: quality.toString(),
    format
  });

  if (height) {
    params.append('height', height.toString());
  }

  return `${SUPABASE_URL}/storage/v1/render/image/public/${path}?${params}`;
}

// Progressive image loading
export function ProgressiveImage({ src, alt }) {
  const [currentSrc, setCurrentSrc] = useState(
    getOptimizedImageUrl(src, { width: 40, quality: 10 })
  );

  useEffect(() => {
    // Load low quality immediately
    const lowQuality = new Image();
    lowQuality.src = getOptimizedImageUrl(src, { width: 400, quality: 50 });
    
    lowQuality.onload = () => {
      setCurrentSrc(lowQuality.src);
      
      // Then load high quality
      const highQuality = new Image();
      highQuality.src = getOptimizedImageUrl(src, { width: 1200, quality: 90 });
      highQuality.onload = () => setCurrentSrc(highQuality.src);
    };
  }, [src]);

  return <img src={currentSrc} alt={alt} />;
}

Query Optimization with React Query

Efficient data fetching and caching.

Query Configuration

Optimized Query Settings:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Cache data for 5 minutes
      staleTime: 5 * 60 * 1000,
      
      // Keep in cache for 10 minutes
      gcTime: 10 * 60 * 1000,
      
      // Don't refetch on window focus by default
      refetchOnWindowFocus: false,
      
      // Retry failed requests with exponential backoff
      retry: (failureCount, error) => {
        if (error.status === 404) return false;
        return failureCount < 3;
      },
      
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

Prefetching Strategies

Anticipatory Data Loading:

// Prefetch on hover
export function CrittrCard({ crittr }) {
  const queryClient = useQueryClient();
  
  const handleMouseEnter = () => {
    queryClient.prefetchQuery({
      queryKey: ['crittr', crittr.id],
      queryFn: () => fetchCrittrDetails(crittr.id),
      staleTime: 10 * 1000, // Consider fresh for 10 seconds
    });
  };

  return (
    <Card 
      onMouseEnter={handleMouseEnter}
      onClick={() => navigate(`/crittrs/${crittr.id}`)}
    >
      {/* Card content */}
    </Card>
  );
}

// Prefetch next page
export function usePrefetchNextPage(currentPage: number) {
  const queryClient = useQueryClient();
  
  useEffect(() => {
    const nextPage = currentPage + 1;
    
    queryClient.prefetchQuery({
      queryKey: ['crittrs', { page: nextPage }],
      queryFn: () => fetchCrittrs({ page: nextPage }),
    });
  }, [currentPage, queryClient]);
}

Query Deduplication

Preventing Redundant Requests:

// Share queries across components
export function useCrittrData(crittrId: string) {
  // This query will be shared by all components
  return useQuery({
    queryKey: ['crittr', crittrId],
    queryFn: () => fetchCrittr(crittrId),
    
    // Use cached data as placeholder while fetching
    placeholderData: () => {
      // Try to use data from list query
      const listData = queryClient.getQueryData(['crittrs']);
      return listData?.find(c => c.id === crittrId);
    },
  });
}

Virtual Scrolling Implementation

Efficiently rendering large lists.

Basic Virtual Scrolling

Using TanStack Virtual:

import { useVirtualizer } from '@tanstack/react-virtual';

export function VirtualCrittrList({ crittrs }) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: crittrs.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100, // Estimated item height
    overscan: 5, // Render 5 items outside viewport
  });

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <CrittrCard crittr={crittrs[virtualItem.index]} />
          
        ))}
      
    
  );
}

Dynamic Height Virtual Scrolling

Variable Item Heights:

export function DynamicVirtualList({ items, renderItem }) {
  const [measurements, setMeasurements] = useState<Record<number, number>>({});
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: useCallback(
      (index) => measurements[index] || 100,
      [measurements]
    ),
    measureElement: useCallback(
      (element, entry, instance) => {
        const index = entry.index;
        const height = element.getBoundingClientRect().height;
        
        if (measurements[index] !== height) {
          setMeasurements(prev => ({ ...prev, [index]: height }));
          instance.measureElement(element);
        }
      },
      [measurements]
    ),
  });

  return (
    <div ref={parentRef} className="h-full overflow-auto">
      {/* Virtual list implementation */}
    
  );
}

Bundle Size Optimization

Keeping your JavaScript lean.

Tree Shaking

Removing Unused Code:

// ❌ Bad: Import entire library
import * as _ from 'lodash';
const result = _.debounce(fn, 300);

// ✅ Good: Import only what you need
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// ❌ Bad: Import entire icon library
import * as Icons from 'lucide-react';
<Icons.Home />

// ✅ Good: Import specific icons
import { Home } from 'lucide-react';
<Home />

Bundle Analysis

Vite Configuration:

// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Separate vendor chunks
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'query-vendor': ['@tanstack/react-query'],
          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          'utils': ['date-fns', 'clsx', 'tailwind-merge'],
        },
      },
    },
    // Enable minification
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
  plugins: [
    // Bundle analyzer
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
});

Dynamic Imports for Large Libraries

Load Heavy Dependencies on Demand:

// Chart library loaded only when needed
async function showChart(data: ChartData) {
  const { Chart } = await import('chart.js/auto');
  
  const ctx = document.getElementById('myChart');
  new Chart(ctx, {
    type: 'line',
    data: data,
  });
}

// PDF generation loaded on export
async function exportPDF() {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  // Generate PDF
}

Memory Leak Prevention

Keeping memory usage under control.

Cleanup Patterns

Proper Resource Cleanup:

export function useEventListener(
  event: string,
  handler: EventListener,
  element = window
) {
  const savedHandler = useRef(handler);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const isSupported = element && element.addEventListener;
    if (!isSupported) return;

    const eventListener = (event: Event) => savedHandler.current(event);
    
    // Add listener
    element.addEventListener(event, eventListener);

    // Cleanup function
    return () => {
      element.removeEventListener(event, eventListener);
    };
  }, [event, element]);
}

Observer Cleanup

Managing Observers:

export function useIntersectionObserver(
  ref: RefObject<Element>,
  options?: IntersectionObserverInit
) {
  const [isIntersecting, setIntersecting] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIntersecting(entry.isIntersecting);
    }, options);

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

    // Cleanup
    return () => {
      observer.disconnect();
    };
  }, [ref, options]);

  return isIntersecting;
}

WeakMap for Object References

Preventing Memory Leaks:

// Use WeakMap for object metadata
const crittrMetadata = new WeakMap<Crittr, Metadata>();

export function getCrittrMetadata(crittr: Crittr): Metadata {
  if (!crittrMetadata.has(crittr)) {
    crittrMetadata.set(crittr, computeMetadata(crittr));
  }
  return crittrMetadata.get(crittr)!;
}

// Objects can be garbage collected when no longer referenced

Performance Monitoring Tools

Measuring and tracking performance.

Web Vitals Monitoring

Real User Monitoring:

import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals';

function sendToAnalytics(metric: Metric) {
  // Send to your analytics endpoint
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  // Use sendBeacon for reliability
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/analytics', body);
  }
}

// Monitor Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
getFCP(sendToAnalytics);

Custom Performance Marks

Application-Specific Metrics:

export class PerformanceMonitor {
  mark(name: string) {
    performance.mark(name);
  }

  measure(name: string, startMark: string, endMark?: string) {
    performance.measure(name, startMark, endMark);
    
    const measure = performance.getEntriesByName(name, 'measure')[0];
    
    // Log to analytics
    this.logMetric({
      name,
      duration: measure.duration,
      timestamp: Date.now(),
    });
  }

  // Track component render time
  trackComponentRender(componentName: string) {
    const startMark = `${componentName}-start`;
    const endMark = `${componentName}-end`;
    
    this.mark(startMark);
    
    return () => {
      this.mark(endMark);
      this.measure(`${componentName}-render`, startMark, endMark);
    };
  }
}

// Usage in component
export function ExpensiveComponent() {
  useEffect(() => {
    const endTracking = performanceMonitor.trackComponentRender('ExpensiveComponent');
    return endTracking;
  }, []);

  return <div>...;
}

React DevTools Profiler

Component Performance Analysis:

import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number
) {
  // Log render performance
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
  
  // Track slow renders
  if (actualDuration > 16) { // Slower than 60fps
    logSlowRender({
      component: id,
      phase,
      duration: actualDuration,
    });
  }
}

export function ProfiledApp() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <App />
    </Profiler>
  );
}

Mobile Performance Considerations

Optimizing for mobile devices.

Touch Responsiveness

Optimizing Touch Interactions:

// Use passive event listeners
export function usePassiveTouchHandlers(ref: RefObject<HTMLElement>) {
  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const options = { passive: true };
    
    const handleTouchStart = (e: TouchEvent) => {
      // Handle touch
    };

    element.addEventListener('touchstart', handleTouchStart, options);
    
    return () => {
      element.removeEventListener('touchstart', handleTouchStart);
    };
  }, [ref]);
}

// Debounce expensive operations
export function useDeboucedTouch(handler: Function, delay = 100) {
  const timeoutRef = useRef<NodeJS.Timeout>();

  const debouncedHandler = useCallback((...args) => {
    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => handler(...args), delay);
  }, [handler, delay]);

  return debouncedHandler;
}

Mobile-Specific Optimizations

Reduced Motion and Data Saver:

// Respect user preferences
export function useReducedMotion() {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(
    window.matchMedia('(prefers-reduced-motion: reduce)').matches
  );

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    
    const handleChange = (e: MediaQueryListEvent) => {
      setPrefersReducedMotion(e.matches);
    };

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  return prefersReducedMotion;
}

// Data saver mode
export function useDataSaver() {
  const [saveData, setSaveData] = useState(false);

  useEffect(() => {
    // Check connection type
    const connection = navigator.connection;
    
    if (connection) {
      setSaveData(
        connection.saveData || 
        connection.effectiveType === 'slow-2g' ||
        connection.effectiveType === '2g'
      );
    }
  }, []);

  return saveData;
}

// Conditional loading based on connection
export function AdaptiveImage({ src, alt }) {
  const saveData = useDataSaver();
  
  if (saveData) {
    // Load low quality or placeholder
    return <img src={getLowQualityUrl(src)} alt={alt} />;
  }
  
  return <ResponsiveImage src={src} alt={alt} />;
}

Database Query Optimization

Efficient data fetching from Supabase.

Query Optimization

Efficient Database Queries:

// ❌ Bad: Fetching everything
const { data } = await supabase
  .from('crittrs')
  .select('*');

// ✅ Good: Select only needed fields
const { data } = await supabase
  .from('crittrs')
  .select('id, name, species, last_fed')
  .eq('haven_id', havenId)
  .order('name')
  .limit(20);

// ❌ Bad: N+1 queries
for (const crittr of crittrs) {
  const logs = await supabase
    .from('logs')
    .select('*')
    .eq('crittr_id', crittr.id);
}

// ✅ Good: Single query with join
const { data } = await supabase
  .from('crittrs')
  .select(`
    id,
    name,
    logs (
      id,
      type,
      created_at
    )
  `)
  .eq('haven_id', havenId);

Pagination Strategies

Efficient Data Loading:

// Cursor-based pagination
export async function fetchCrittrsPaginated(cursor?: string, limit = 20) {
  let query = supabase
    .from('crittrs')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(limit);

  if (cursor) {
    query = query.lt('created_at', cursor);
  }

  const { data, error } = await query;

  return {
    items: data,
    nextCursor: data?.[data.length - 1]?.created_at,
  };
}

// Infinite scroll implementation
export function useInfiniteCrittrs() {
  return useInfiniteQuery({
    queryKey: ['crittrs', 'infinite'],
    queryFn: ({ pageParam }) => fetchCrittrsPaginated(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: undefined,
  });
}

Caching Strategies

Smart caching for instant responses.

Service Worker Caching

PWA Cache Strategy:

// service-worker.js
const CACHE_NAME = 'crittrhavens-v1';
const urlsToCache = [
  '/',
  '/static/css/main.css',
  '/static/js/bundle.js',
  '/offline.html',
];

// Install and cache essential resources
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// Network-first strategy for API calls
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // Clone and cache the response
          const responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then(cache => cache.put(event.request, responseToCache));
          return response;
        })
        .catch(() => {
          // Fallback to cache if offline
          return caches.match(event.request);
        })
    );
  }
});

Local Storage Caching

Persistent Client-Side Cache:

class LocalCache {
  private prefix = 'crittrhavens_cache_';

  set(key: string, data: any, ttl = 3600000) { // 1 hour default
    const item = {
      data,
      timestamp: Date.now(),
      ttl,
    };
    
    try {
      localStorage.setItem(this.prefix + key, JSON.stringify(item));
    } catch (e) {
      // Handle quota exceeded
      this.cleanup();
    }
  }

  get(key: string) {
    const item = localStorage.getItem(this.prefix + key);
    if (!item) return null;

    const parsed = JSON.parse(item);
    
    // Check if expired
    if (Date.now() - parsed.timestamp > parsed.ttl) {
      localStorage.removeItem(this.prefix + key);
      return null;
    }

    return parsed.data;
  }

  cleanup() {
    // Remove oldest entries if storage is full
    const items = Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .map(key => ({
        key,
        timestamp: JSON.parse(localStorage.getItem(key)!).timestamp,
      }))
      .sort((a, b) => a.timestamp - b.timestamp);

    // Remove oldest 25%
    const toRemove = Math.floor(items.length * 0.25);
    items.slice(0, toRemove).forEach(item => {
      localStorage.removeItem(item.key);
    });
  }
}

Best Practices

Performance optimization guidelines.

Performance Checklist

Before deployment:

  • Code splitting implemented for routes
  • Images lazy loaded and optimized
  • Bundle size under 500KB
  • Core Web Vitals passing
  • Virtual scrolling for long lists
  • Memory leaks prevented
  • Database queries optimized
  • Caching strategy implemented
  • Mobile performance tested
  • Performance monitoring active

Do's and Don'ts

✅ DO:

  • Measure performance before optimizing
  • Use production builds for testing
  • Optimize critical rendering path
  • Implement progressive enhancement
  • Cache expensive computations

❌ DON'T:

  • Optimize prematurely
  • Block the main thread
  • Load unnecessary resources
  • Ignore mobile performance
  • Cache sensitive data

Common Patterns

Reusable performance solutions.

Debounce and Throttle

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

export function useThrottle<T>(value: T, interval: number): T {
  const [throttledValue, setThrottledValue] = useState(value);
  const lastUpdated = useRef(Date.now());

  useEffect(() => {
    const now = Date.now();
    
    if (now - lastUpdated.current >= interval) {
      lastUpdated.current = now;
      setThrottledValue(value);
    } else {
      const timer = setTimeout(() => {
        lastUpdated.current = Date.now();
        setThrottledValue(value);
      }, interval - (now - lastUpdated.current));
      
      return () => clearTimeout(timer);
    }
  }, [value, interval]);

  return throttledValue;
}

Memoization Patterns

// Memoize expensive computations
export const calculateStats = memoize((crittrs: Crittr[]) => {
  // Expensive calculation
  return {
    total: crittrs.length,
    bySpecies: groupBy(crittrs, 'species'),
    averageAge: mean(crittrs.map(c => c.age)),
  };
});

// React memo with comparison
export const CrittrCard = memo(
  ({ crittr }: { crittr: Crittr }) => {
    return <Card>{/* Component */}</Card>;
  },
  (prevProps, nextProps) => {
    // Only re-render if crittr data changes
    return prevProps.crittr.id === nextProps.crittr.id &&
           prevProps.crittr.updatedAt === nextProps.crittr.updatedAt;
  }
);

Next Steps

Continue optimizing your application.


Making your reptile care app faster than a striking snake. 🐍