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
- Review best practices for optimal usage
- Learn about custom hook patterns for advanced usage
- Explore the hook reference for detailed usage examples