FoundryKit

Performance Optimization

Performance optimization techniques for FoundryKit hooks

Performance Optimization

Learn how to optimize the performance of your applications when using FoundryKit hooks.

Memoization Strategies

Memoize Expensive Computations

Use useMemo to cache expensive calculations:

import { useDebouncedValue } from '@foundrykit/hooks'
import { useState, useMemo, useCallback } from 'react'

function OptimizedSearch({ items }) {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebouncedValue(query, 300)

  // Memoize filtered results
  const filteredItems = useMemo(() => {
    if (!debouncedQuery) return items
    
    return items.filter(item =>
      item.name.toLowerCase().includes(debouncedQuery.toLowerCase()) ||
      item.description.toLowerCase().includes(debouncedQuery.toLowerCase())
    )
  }, [items, debouncedQuery])

  // Memoize sorted results
  const sortedItems = useMemo(() => {
    return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name))
  }, [filteredItems])

  // Memoize callbacks
  const handleSearch = useCallback((searchQuery: string) => {
    setQuery(searchQuery)
  }, [])

  const handleClear = useCallback(() => {
    setQuery('')
  }, [])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={handleClear}>Clear</button>
      
      <div>
        {sortedItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  )
}

Memoize Hook Results

Cache the results of custom hooks to prevent unnecessary re-renders:

import { useLocalStorage, useMediaQuery } from '@foundrykit/hooks'
import { useMemo } from 'react'

function useOptimizedTheme() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')

  // Memoize theme configuration
  const themeConfig = useMemo(() => {
    const actualTheme = theme === 'system' ? (prefersDark ? 'dark' : 'light') : theme
    
    return {
      theme: actualTheme,
      isDark: actualTheme === 'dark',
      colors: actualTheme === 'dark' ? darkColors : lightColors,
      className: `theme-${actualTheme}`
    }
  }, [theme, prefersDark])

  return {
    theme,
    setTheme,
    ...themeConfig
  }
}

Lazy Loading and Code Splitting

Lazy Load Hooks

Load hooks only when needed:

import { lazy, Suspense } from 'react'
import { useLocalStorage } from '@foundrykit/hooks'

// Lazy load expensive hooks
const useExpensiveHook = lazy(() => 
  import('./useExpensiveHook').then(m => ({ default: m.useExpensiveHook }))
)

function ConditionalHookUsage({ shouldUseExpensiveHook }) {
  const [basicData, setBasicData] = useLocalStorage('basicData', {})

  return (
    <div>
      <div>Basic data: {JSON.stringify(basicData)}</div>
      
      {shouldUseExpensiveHook && (
        <Suspense fallback={<div>Loading expensive hook...</div>}>
          <ExpensiveComponent />
        </Suspense>
      )}
    </div>
  )
}

function ExpensiveComponent() {
  const expensiveData = useExpensiveHook()
  return <div>Expensive data: {JSON.stringify(expensiveData)}</div>
}

Dynamic Hook Loading

Load hooks based on conditions:

import { useLocalStorage, useMediaQuery } from '@foundrykit/hooks'
import { useState, useEffect } from 'react'

function useConditionalHooks(enabled: boolean) {
  const [hooks, setHooks] = useState({})

  useEffect(() => {
    if (enabled) {
      // Dynamically import hooks when needed
      import('@foundrykit/hooks').then(hooksModule => {
        setHooks(hooksModule)
      })
    }
  }, [enabled])

  return hooks
}

function ConditionalHookComponent({ featureEnabled }) {
  const hooks = useConditionalHooks(featureEnabled)
  
  if (!hooks.useLocalStorage) {
    return <div>Loading hooks...</div>
  }

  const [data, setData] = hooks.useLocalStorage('data', {})
  
  return (
    <div>
      <button onClick={() => setData({ timestamp: Date.now() })}>
        Update Data
      </button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

Debouncing and Throttling

Optimize Search with Debouncing

Use debouncing to reduce API calls:

import { useDebouncedValue } from '@foundrykit/hooks'
import { useState, useEffect, useCallback } from 'react'

function useOptimizedSearch(searchFunction: (query: string) => Promise<any[]>) {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  
  // Use different debounce delays based on query length
  const debounceDelay = query.length > 3 ? 300 : 500
  const debouncedQuery = useDebouncedValue(query, debounceDelay)

  useEffect(() => {
    if (debouncedQuery && debouncedQuery.length >= 2) {
      setIsLoading(true)
      
      const controller = new AbortController()
      
      searchFunction(debouncedQuery)
        .then(searchResults => {
          if (!controller.signal.aborted) {
            setResults(searchResults)
          }
        })
        .catch(error => {
          if (!controller.signal.aborted) {
            console.error('Search failed:', error)
          }
        })
        .finally(() => {
          if (!controller.signal.aborted) {
            setIsLoading(false)
          }
        })

      return () => controller.abort()
    } else {
      setResults([])
    }
  }, [debouncedQuery, searchFunction])

  const clearSearch = useCallback(() => {
    setQuery('')
    setResults([])
  }, [])

  return {
    query,
    setQuery,
    results,
    isLoading,
    clearSearch
  }
}

Throttle Expensive Operations

Throttle operations that don't need to be debounced:

import { useLocalStorage } from '@foundrykit/hooks'
import { useState, useCallback, useRef } from 'react'

function useThrottledUpdate<T>(
  key: string,
  initialValue: T,
  throttleMs: number = 1000
) {
  const [value, setValue] = useLocalStorage(key, initialValue)
  const lastUpdateRef = useRef(0)
  const pendingUpdateRef = useRef<T | null>(null)

  const throttledSetValue = useCallback((newValue: T) => {
    const now = Date.now()
    
    if (now - lastUpdateRef.current >= throttleMs) {
      setValue(newValue)
      lastUpdateRef.current = now
      pendingUpdateRef.current = null
    } else {
      pendingUpdateRef.current = newValue
      
      setTimeout(() => {
        if (pendingUpdateRef.current !== null) {
          setValue(pendingUpdateRef.current)
          lastUpdateRef.current = Date.now()
          pendingUpdateRef.current = null
        }
      }, throttleMs - (now - lastUpdateRef.current))
    }
  }, [setValue, throttleMs])

  return [value, throttledSetValue] as const
}

// Usage
function ThrottledForm() {
  const [formData, setFormData] = useThrottledUpdate('formData', {
    name: '',
    email: '',
    message: ''
  }, 2000) // Update localStorage every 2 seconds

  return (
    <form className="space-y-4">
      <input
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        placeholder="Name"
      />
      <input
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
      />
      <textarea
        value={formData.message}
        onChange={(e) => setFormData({ ...formData, message: e.target.value })}
        placeholder="Message"
      />
    </form>
  )
}

Memory Management

Cleanup and Unmounting

Properly clean up resources when components unmount:

import { useLocalStorage, useDebouncedValue } from '@foundrykit/hooks'
import { useState, useEffect, useRef } from 'react'

function useSearchWithCleanup() {
  const [query, setQuery] = useLocalStorage('searchQuery', '')
  const debouncedQuery = useDebouncedValue(query, 300)
  const [results, setResults] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  const mountedRef = useRef(true)

  useEffect(() => {
    return () => {
      mountedRef.current = false
    }
  }, [])

  useEffect(() => {
    if (debouncedQuery && mountedRef.current) {
      setIsLoading(true)
      
      performSearch(debouncedQuery)
        .then(searchResults => {
          if (mountedRef.current) {
            setResults(searchResults)
          }
        })
        .catch(error => {
          if (mountedRef.current) {
            console.error('Search failed:', error)
          }
        })
        .finally(() => {
          if (mountedRef.current) {
            setIsLoading(false)
          }
        })
    }
  }, [debouncedQuery])

  return {
    query,
    setQuery,
    results,
    isLoading
  }
}

Optimize localStorage Usage

Limit localStorage operations to prevent performance issues:

import { useLocalStorage } from '@foundrykit/hooks'
import { useCallback, useRef } from 'react'

function useOptimizedLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useLocalStorage(key, initialValue)
  const lastSavedRef = useRef<T>(initialValue)
  const saveTimeoutRef = useRef<NodeJS.Timeout>()

  const optimizedSetValue = useCallback((newValue: T) => {
    // Update state immediately
    setValue(newValue)
    
    // Debounce localStorage writes
    if (saveTimeoutRef.current) {
      clearTimeout(saveTimeoutRef.current)
    }
    
    saveTimeoutRef.current = setTimeout(() => {
      if (JSON.stringify(newValue) !== JSON.stringify(lastSavedRef.current)) {
        localStorage.setItem(key, JSON.stringify(newValue))
        lastSavedRef.current = newValue
      }
    }, 1000)
  }, [key, setValue])

  return [value, optimizedSetValue] as const
}

Bundle Size Optimization

Tree Shaking

Ensure hooks are properly tree-shaken:

// ✅ Good - Import specific hooks
import { useLocalStorage } from '@foundrykit/hooks'

// ❌ Avoid - Import entire package
import * as hooks from '@foundrykit/hooks'

function OptimizedComponent() {
  const [data, setData] = useLocalStorage('data', {})
  return <div>{JSON.stringify(data)}</div>
}

Dynamic Imports

Use dynamic imports for conditional hook usage:

import { useState, useEffect } from 'react'

function useDynamicHook(hookName: string, enabled: boolean) {
  const [hook, setHook] = useState(null)

  useEffect(() => {
    if (enabled) {
      import('@foundrykit/hooks')
        .then(module => {
          setHook(module[hookName])
        })
        .catch(error => {
          console.error(`Failed to load hook ${hookName}:`, error)
        })
    }
  }, [hookName, enabled])

  return hook
}

function DynamicHookUsage() {
  const useLocalStorage = useDynamicHook('useLocalStorage', true)
  
  if (!useLocalStorage) {
    return <div>Loading hook...</div>
  }

  const [data, setData] = useLocalStorage('data', {})
  return <div>{JSON.stringify(data)}</div>
}

Performance Monitoring

Hook Performance Tracking

Track hook performance in development:

import { useLocalStorage, useDebouncedValue } from '@foundrykit/hooks'
import { useEffect, useRef } from 'react'

function usePerformanceTracking(hookName: string) {
  const startTimeRef = useRef(performance.now())
  const renderCountRef = useRef(0)

  useEffect(() => {
    renderCountRef.current++
    const endTime = performance.now()
    const duration = endTime - startTimeRef.current

    if (process.env.NODE_ENV === 'development') {
      console.log(`${hookName} render #${renderCountRef.current}: ${duration.toFixed(2)}ms`)
    }

    startTimeRef.current = endTime
  })
}

function useOptimizedLocalStorageWithTracking<T>(key: string, initialValue: T) {
  usePerformanceTracking('useLocalStorage')
  return useLocalStorage(key, initialValue)
}

Memory Usage Monitoring

Monitor memory usage of hooks:

import { useLocalStorage } from '@foundrykit/hooks'
import { useEffect } from 'react'

function useMemoryMonitoring() {
  useEffect(() => {
    if (process.env.NODE_ENV === 'development' && 'memory' in performance) {
      const memory = (performance as any).memory
      console.log('Memory usage:', {
        used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
        total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
        limit: `${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`
      })
    }
  })
}

function MonitoredComponent() {
  useMemoryMonitoring()
  const [data, setData] = useLocalStorage('largeData', {})
  
  return <div>Component with memory monitoring</div>
}

Best Practices Summary

Do's

  • ✅ Use useMemo for expensive computations
  • ✅ Use useCallback for function references
  • ✅ Debounce search operations
  • ✅ Clean up resources on unmount
  • ✅ Tree-shake imports
  • ✅ Monitor performance in development

Don'ts

  • ❌ Avoid unnecessary re-renders
  • ❌ Don't store large objects in localStorage
  • ❌ Don't create hooks inside render functions
  • ❌ Don't ignore cleanup in useEffect
  • ❌ Don't import entire packages when you only need specific hooks

Next Steps