FoundryKit

Performance Tips

Performance optimization tips for FoundryKit utilities

Performance Tips

Learn how to optimize performance when using FoundryKit utilities in your applications.

Bundle Size Optimization

Tree Shaking

Import only the utilities you need to minimize bundle size.

// ❌ Avoid importing the entire package
import * as utils from '@foundrykit/utils'

// ✅ Use named imports for tree shaking
import { cn, debounce, formatDate } from '@foundrykit/utils'

// ✅ Or import from specific modules
import { cn } from '@foundrykit/utils/cn'
import { debounce } from '@foundrykit/utils/function'
import { formatDate } from '@foundrykit/utils/date'

Dynamic Imports

Use dynamic imports for utilities that aren't needed immediately.

import React, { useState, lazy, Suspense } from 'react'

// Lazy load heavy utilities
const LazyDataProcessor = lazy(() => import('./DataProcessor'))

// Dynamic import for occasional use
async function processLargeDataset(data: any[]) {
  const { chunk, sortBy, groupBy } = await import('@foundrykit/utils')
  
  // Process data only when needed
  const chunks = chunk(data, 1000)
  return chunks.map(chunk => sortBy(chunk, 'timestamp'))
}

export function DataDashboard() {
  const [showProcessor, setShowProcessor] = useState(false)

  return (
    <div>
      <button onClick={() => setShowProcessor(true)}>
        Load Data Processor
      </button>
      
      {showProcessor && (
        <Suspense fallback={<div>Loading processor...</div>}>
          <LazyDataProcessor />
        </Suspense>
      )}
    </div>
  )
}

Bundle Analysis

Monitor your bundle size to track utility usage.

# Analyze bundle size
npm run build -- --analyze

# Check specific utility impact
npm run bundle-analyzer

Memory Management

Memoization Best Practices

Use memoization strategically to avoid memory leaks.

import { memoize, debounce } from '@foundrykit/utils'

// ✅ Good: Memoize pure functions with stable inputs
const calculateExpensiveValue = memoize((data: number[]) => {
  return data.reduce((sum, val) => sum + Math.pow(val, 2), 0)
})

// ✅ Good: Clear cache when data changes significantly
const processUserData = memoize((users: User[]) => {
  return users.map(user => ({
    ...user,
    displayName: `${user.firstName} ${user.lastName}`
  }))
})

// Clear cache when users change
useEffect(() => {
  processUserData.cache.clear()
}, [usersVersion])

// ❌ Avoid: Memoizing functions with constantly changing inputs
const badMemoization = memoize((timestamp: number, data: any) => {
  // This will create a new cache entry for every timestamp
  return processData(data)
})

// ✅ Better: Memoize the stable part only
const goodMemoization = memoize((data: any) => {
  return processData(data)
})

Debounce and Throttle Cleanup

Properly clean up debounced and throttled functions.

import React, { useEffect, useMemo } from 'react'
import { debounce, throttle } from '@foundrykit/utils'

export function SearchComponent() {
  const [query, setQuery] = useState('')

  // Create debounced function with cleanup
  const debouncedSearch = useMemo(
    () => debounce((searchQuery: string) => {
      performSearch(searchQuery)
    }, 300),
    []
  )

  const throttledScroll = useMemo(
    () => throttle(() => {
      updateScrollPosition()
    }, 100),
    []
  )

  useEffect(() => {
    // Add scroll listener
    window.addEventListener('scroll', throttledScroll)

    // Cleanup on unmount
    return () => {
      window.removeEventListener('scroll', throttledScroll)
      debouncedSearch.cancel() // Cancel pending debounced calls
      throttledScroll.cancel() // Cancel pending throttled calls
    }
  }, [debouncedSearch, throttledScroll])

  return (
    <input
      onChange={(e) => {
        setQuery(e.target.value)
        debouncedSearch(e.target.value)
      }}
      value={query}
    />
  )
}

Computation Optimization

Efficient Data Processing

Optimize data processing operations for large datasets.

import { chunk, groupBy, sortBy, unique } from '@foundrykit/utils'

// ✅ Process large datasets in chunks
function processLargeDataset(data: any[], chunkSize: number = 1000) {
  const chunks = chunk(data, chunkSize)
  
  return chunks.map(chunk => {
    // Process each chunk separately to avoid blocking the main thread
    return new Promise(resolve => {
      setTimeout(() => {
        const processed = chunk.map(item => processItem(item))
        resolve(processed)
      }, 0)
    })
  })
}

// ✅ Use Web Workers for CPU-intensive operations
function processDataInWorker(data: any[]) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('/data-processor-worker.js')
    
    worker.postMessage({ data })
    
    worker.onmessage = (e) => {
      resolve(e.data.result)
      worker.terminate()
    }
    
    worker.onerror = (error) => {
      reject(error)
      worker.terminate()
    }
  })
}

// ✅ Optimize sorting and grouping operations
function optimizeDataOperations(data: any[]) {
  // Sort once, use multiple times
  const sortedData = sortBy(data, 'timestamp')
  
  // Group the already sorted data
  const groupedData = groupBy(sortedData, 'category')
  
  // Remove duplicates efficiently
  const uniqueData = unique(sortedData, 'id')
  
  return { sortedData, groupedData, uniqueData }
}

// ❌ Avoid: Multiple expensive operations on the same data
function inefficientOperations(data: any[]) {
  const sorted1 = sortBy(data, 'timestamp') // Sort #1
  const grouped = groupBy(data, 'category') // Iterates again
  const sorted2 = sortBy(data, 'name') // Sort #2
  const unique = unique(data, 'id') // Iterates again
  
  return { sorted1, grouped, sorted2, unique }
}

Lazy Evaluation

Implement lazy evaluation for expensive operations.

import { memoize } from '@foundrykit/utils'

class LazyDataProcessor {
  private data: any[]
  private _sorted?: any[]
  private _grouped?: Record<string, any[]>
  private _filtered?: any[]

  constructor(data: any[]) {
    this.data = data
  }

  // Lazy getter for sorted data
  get sorted() {
    if (!this._sorted) {
      this._sorted = sortBy(this.data, 'timestamp')
    }
    return this._sorted
  }

  // Lazy getter for grouped data
  get grouped() {
    if (!this._grouped) {
      this._grouped = groupBy(this.data, 'category')
    }
    return this._grouped
  }

  // Lazy method with memoization
  getFiltered = memoize((predicate: (item: any) => boolean) => {
    return this.data.filter(predicate)
  })

  // Reset cached values when data changes
  updateData(newData: any[]) {
    this.data = newData
    this._sorted = undefined
    this._grouped = undefined
    this.getFiltered.cache.clear()
  }
}

// Usage
const processor = new LazyDataProcessor(largeDataset)

// Only computed when accessed
const sortedData = processor.sorted
const groupedData = processor.grouped
const filteredData = processor.getFiltered(item => item.active)

React Performance Integration

Optimized Hook Usage

Integrate utilities with React hooks efficiently.

import React, { useMemo, useCallback, useState } from 'react'
import { debounce, sortBy, groupBy, unique } from '@foundrykit/utils'

interface UseDataProcessorOptions {
  sortBy?: string
  groupBy?: string
  filterFn?: (item: any) => boolean
  debounceMs?: number
}

function useDataProcessor(data: any[], options: UseDataProcessorOptions = {}) {
  const {
    sortBy: sortField,
    groupBy: groupField,
    filterFn,
    debounceMs = 300
  } = options

  const [query, setQuery] = useState('')
  const [debouncedQuery, setDebouncedQuery] = useState('')

  // Debounce query updates
  const debouncedSetQuery = useMemo(
    () => debounce((value: string) => {
      setDebouncedQuery(value)
    }, debounceMs),
    [debounceMs]
  )

  // Update query and trigger debounced update
  const updateQuery = useCallback((value: string) => {
    setQuery(value)
    debouncedSetQuery(value)
  }, [debouncedSetQuery])

  // Process data with memoization
  const processedData = useMemo(() => {
    let result = data

    // Apply text filter
    if (debouncedQuery) {
      const queryLower = debouncedQuery.toLowerCase()
      result = result.filter(item =>
        Object.values(item).some(value =>
          String(value).toLowerCase().includes(queryLower)
        )
      )
    }

    // Apply custom filter
    if (filterFn) {
      result = result.filter(filterFn)
    }

    // Sort if specified
    if (sortField) {
      result = sortBy(result, sortField)
    }

    return result
  }, [data, debouncedQuery, filterFn, sortField])

  // Group data if specified
  const groupedData = useMemo(() => {
    if (!groupField) return null
    return groupBy(processedData, groupField)
  }, [processedData, groupField])

  // Get unique values for a field
  const getUniqueValues = useCallback((field: string) => {
    return unique(data.map(item => item[field]))
  }, [data])

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      debouncedSetQuery.cancel()
    }
  }, [debouncedSetQuery])

  return {
    query,
    updateQuery,
    processedData,
    groupedData,
    getUniqueValues,
    isProcessing: query !== debouncedQuery
  }
}

// Usage in component
export function DataTable({ data }: { data: any[] }) {
  const {
    query,
    updateQuery,
    processedData,
    groupedData,
    getUniqueValues,
    isProcessing
  } = useDataProcessor(data, {
    sortBy: 'name',
    groupBy: 'category',
    debounceMs: 250
  })

  const categories = useMemo(() => getUniqueValues('category'), [getUniqueValues])

  return (
    <div>
      <input
        value={query}
        onChange={(e) => updateQuery(e.target.value)}
        placeholder="Search..."
        className="mb-4 p-2 border rounded"
      />
      
      {isProcessing && (
        <div className="text-sm text-gray-500">Processing...</div>
      )}
      
      <div className="grid gap-4">
        {groupedData ? (
          Object.entries(groupedData).map(([category, items]) => (
            <div key={category}>
              <h3 className="font-bold">{category}</h3>
              <div className="grid gap-2">
                {items.map((item, index) => (
                  <div key={index} className="p-2 border rounded">
                    {item.name}
                  </div>
                ))}
              </div>
            </div>
          ))
        ) : (
          processedData.map((item, index) => (
            <div key={index} className="p-2 border rounded">
              {item.name}
            </div>
          ))
        )}
      </div>
    </div>
  )
}

Virtual Scrolling with Utilities

Implement virtual scrolling for large lists using utilities.

import React, { useMemo, useState, useCallback } from 'react'
import { chunk, sortBy } from '@foundrykit/utils'

interface VirtualizedListProps {
  items: any[]
  itemHeight: number
  containerHeight: number
  renderItem: (item: any, index: number) => React.ReactNode
  sortField?: string
}

export function VirtualizedList({
  items,
  itemHeight,
  containerHeight,
  renderItem,
  sortField
}: VirtualizedListProps) {
  const [scrollTop, setScrollTop] = useState(0)

  // Sort items if needed
  const sortedItems = useMemo(() => {
    return sortField ? sortBy(items, sortField) : items
  }, [items, sortField])

  // Calculate visible range
  const visibleRange = useMemo(() => {
    const startIndex = Math.floor(scrollTop / itemHeight)
    const endIndex = Math.min(
      startIndex + Math.ceil(containerHeight / itemHeight) + 1,
      sortedItems.length
    )
    return { startIndex, endIndex }
  }, [scrollTop, itemHeight, containerHeight, sortedItems.length])

  // Get visible items
  const visibleItems = useMemo(() => {
    return sortedItems.slice(visibleRange.startIndex, visibleRange.endIndex)
  }, [sortedItems, visibleRange])

  // Handle scroll
  const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop)
  }, [])

  const totalHeight = sortedItems.length * itemHeight
  const offsetY = visibleRange.startIndex * itemHeight

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) =>
            renderItem(item, visibleRange.startIndex + index)
          )}
        </div>
      </div>
    </div>
  )
}

Performance Monitoring

Utility Performance Tracking

Monitor the performance of utility functions.

import { memoize, debounce } from '@foundrykit/utils'

// Performance monitoring wrapper
function withPerformanceTracking<T extends (...args: any[]) => any>(
  fn: T,
  name: string
): T {
  return ((...args: any[]) => {
    const start = performance.now()
    const result = fn(...args)
    const end = performance.now()
    
    if (end - start > 10) { // Log slow operations (>10ms)
      console.warn(`Slow utility function ${name}: ${(end - start).toFixed(2)}ms`)
    }
    
    return result
  }) as T
}

// Wrap expensive utilities
const trackedSortBy = withPerformanceTracking(sortBy, 'sortBy')
const trackedGroupBy = withPerformanceTracking(groupBy, 'groupBy')

// Performance-aware memoization
const performanceMemoize = <T extends (...args: any[]) => any>(
  fn: T,
  name: string
) => {
  const memoized = memoize(fn)
  
  return ((...args: any[]) => {
    const start = performance.now()
    const result = memoized(...args)
    const end = performance.now()
    
    const cacheHit = end - start < 1 // Assume cache hit if very fast
    
    if (!cacheHit && end - start > 50) {
      console.warn(`Slow memoized function ${name}: ${(end - start).toFixed(2)}ms`)
    }
    
    return result
  }) as T
}

Bundle Size Monitoring

Track utility bundle impact in your build process.

// webpack.config.js or similar
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        utils: {
          test: /[\\/]node_modules[\\/]@foundrykit[\\/]utils/,
          name: 'foundrykit-utils',
          chunks: 'all'
        }
      }
    }
  }
}

Performance Best Practices

Do's and Don'ts

// ✅ DO: Use utilities efficiently
const processData = memoize((data: any[]) => {
  // Combine multiple operations efficiently
  const sorted = sortBy(data, 'timestamp')
  const grouped = groupBy(sorted, 'category')
  return { sorted, grouped }
})

// ❌ DON'T: Create new utility instances in render
function BadComponent({ data }) {
  return (
    <div>
      {data.map(item => {
        // Creates new debounced function on every render
        const debouncedFn = debounce(() => {}, 300)
        return <div key={item.id}>{item.name}</div>
      })}
    </div>
  )
}

// ✅ DO: Create utility instances outside render or with useMemo
function GoodComponent({ data }) {
  const debouncedFn = useMemo(() => debounce(() => {}, 300), [])
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  )
}

// ✅ DO: Use appropriate utility for the task
const largeArray = Array.from({ length: 10000 }, (_, i) => ({ id: i, value: Math.random() }))

// Efficient for large arrays
const chunked = chunk(largeArray, 100)
const sorted = sortBy(largeArray, 'value')

// ❌ DON'T: Use utilities unnecessarily for small datasets
const smallArray = [1, 2, 3, 4, 5]

// Overkill for small arrays
const unnecessaryChunks = chunk(smallArray, 2) // Just use array methods

// ✅ Better for small arrays
const simpleChunks = [smallArray.slice(0, 2), smallArray.slice(2, 4), smallArray.slice(4)]

Performance Checklist

  • Import only needed utilities (tree shaking)
  • Use memoization for expensive computations
  • Clear memoization caches when data changes
  • Cancel debounced/throttled functions on cleanup
  • Process large datasets in chunks
  • Use lazy evaluation for optional computations
  • Monitor bundle size impact
  • Profile performance in development
  • Use appropriate utilities for data size
  • Avoid creating utility instances in render loops

Next Steps