Skip to content

Performance Optimization

Best practices for optimizing useTemporal applications for maximum performance.

Progressive Division Strategy

The divide() function is powerful but can create many objects if used carelessly. Follow these patterns for optimal performance:

❌ Inefficient: Creating Too Many Objects

typescript
// Don't create all subdivisions at once
const year = usePeriod(temporal, 'year')
const allSeconds = year.value
  |> (y) => divide(temporal.adapter, y, 'month')
  |> (months) => months.flatMap(m => divide(temporal.adapter, m, 'day'))
  |> (days) => days.flatMap(d => divide(temporal.adapter, d, 'hour'))
  |> (hours) => hours.flatMap(h => divide(temporal.adapter, h, 'minute'))
  |> (minutes) => minutes.flatMap(m => divide(temporal.adapter, m, 'second'))
// This creates 31,536,000+ objects!

✅ Efficient: Divide Only What You Need

typescript
// Better: Divide progressively based on view requirements
const month = usePeriod(temporal, 'month')
const days = divide(temporal.adapter, month.value, 'day')

// Only divide specific days when needed
if (needHourlyView) {
  const todayHours = divide(temporal.adapter, days[14], 'hour')
}

Memoization Strategies

Using Computed Values

Results are automatically memoized when using reactive computed values:

typescript
import { computed } from '@vue/reactivity'

// Results are cached until dependencies change
const month = usePeriod(temporal, 'month')
const days = computed(() => divide(temporal.adapter, month.value, 'day'))

// days.value is only recalculated when month changes

Manual Memoization for Non-Reactive Code

typescript
// Cache expensive calculations
const cache = new Map()

function getCachedDivision(temporal, period, unit) {
  const key = `${period.start.toISOString()}-${unit}`
  
  if (!cache.has(key)) {
    cache.set(key, divide(temporal.adapter, period, unit))
  }
  
  return cache.get(key)
}

Lazy Loading Patterns

On-Demand Division

Load time divisions only when the user needs them:

typescript
// Calendar component with lazy month loading
const visibleMonths = ref(new Set())

function loadMonth(monthPeriod) {
  const monthKey = monthPeriod.start.toISOString()
  
  if (!visibleMonths.value.has(monthKey)) {
    const days = divide(temporal.adapter, monthPeriod, 'day')
    visibleMonths.value.add(monthKey)
    // Store days for rendering
  }
}

Viewport-Based Loading

For large time ranges, only load what's visible:

typescript
// Year view with viewport detection
function YearCalendar() {
  const [visibleMonths, setVisibleMonths] = useState(new Set())
  
  const handleIntersection = (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const monthIndex = entry.target.dataset.month
        loadMonthData(monthIndex)
      }
    })
  }
  
  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersection)
    // Observe month containers
    return () => observer.disconnect()
  }, [])
}

Bundle Size Optimization

Tree Shaking

Import only what you need:

typescript
// ❌ Imports everything
import * as useTemporal from '@allystudio/usetemporal'

// ✅ Imports only needed functions
import { createTemporal, usePeriod, divide } from '@allystudio/usetemporal'

Adapter Selection

Choose the right adapter for your needs:

typescript
// Smallest bundle: Native adapter (0 dependencies)
import { createTemporal } from '@allystudio/usetemporal'

// Larger bundle: External library adapters
import { createTemporal } from '@allystudio/usetemporal'
import { createLuxonAdapter } from '@allystudio/usetemporal/luxon'

Memory Management

Cleanup Unused Periods

typescript
// Vue example with proper cleanup
onUnmounted(() => {
  // Clear any cached data
  periodCache.clear()
  
  // Stop any reactive effects
  stopWatching()
})

Limit Stored History

typescript
// Keep only recent calculations
const MAX_CACHE_SIZE = 100
const recentCalculations = new Map()

function addToCache(key, value) {
  if (recentCalculations.size >= MAX_CACHE_SIZE) {
    // Remove oldest entry
    const firstKey = recentCalculations.keys().next().value
    recentCalculations.delete(firstKey)
  }
  
  recentCalculations.set(key, value)
}

Reactive Performance

Batch Updates

Group multiple temporal updates:

typescript
// ❌ Multiple reactive updates
temporal.browsing.value = nextMonth
selectedDay.value = firstDayOfMonth
highlightedWeek.value = firstWeek

// ✅ Batch updates
import { batch } from '@vue/reactivity'

batch(() => {
  temporal.browsing.value = nextMonth
  selectedDay.value = firstDayOfMonth
  highlightedWeek.value = firstWeek
})

Debounce Expensive Operations

typescript
import { debounce } from 'lodash-es'

const debouncedCalculation = debounce((temporal, period) => {
  // Expensive calculation
  const analysis = analyzeTimeRange(temporal, period)
  updateUI(analysis)
}, 300)

// In reactive effect
watchEffect(() => {
  debouncedCalculation(temporal, month.value)
})

Rendering Optimization

Virtual Scrolling for Large Ranges

typescript
// Render only visible time periods
function VirtualCalendar({ years }) {
  const rowHeight = 300 // Height of month row
  const visibleRows = 4 // Number of visible months
  
  return (
    <VirtualList
      height={rowHeight * visibleRows}
      itemCount={years * 12}
      itemSize={rowHeight}
      renderItem={({ index, style }) => (
        <div style={style}>
          <MonthCalendar monthIndex={index} />
        </div>
      )}
    />
  )
}

Minimize Re-renders

typescript
// React: Use memo for expensive components
const DayCell = React.memo(({ day, isSelected, onSelect }) => {
  return (
    <div 
      className={isSelected ? 'selected' : ''}
      onClick={() => onSelect(day)}
    >
      {day.date.getDate()}
    </div>
  )
}, (prevProps, nextProps) => {
  // Custom comparison
  return prevProps.day.date.getTime() === nextProps.day.date.getTime() &&
         prevProps.isSelected === nextProps.isSelected
})

Benchmarking

Measure Performance

typescript
function measureDividePerformance() {
  const temporal = createTemporal({ date: new Date() })
  const year = usePeriod(temporal, 'year')
  
  console.time('divide-year-to-days')
  const months = divide(temporal.adapter, year.value, 'month')
  const allDays = months.flatMap(m => divide(temporal.adapter, m, 'day'))
  console.timeEnd('divide-year-to-days')
  
  console.log(`Created ${allDays.length} day objects`)
}

Profile Memory Usage

typescript
if (typeof performance !== 'undefined' && performance.memory) {
  const before = performance.memory.usedJSHeapSize
  
  // Perform operations
  const periods = generateManyPeriods()
  
  const after = performance.memory.usedJSHeapSize
  console.log(`Memory used: ${(after - before) / 1024 / 1024} MB`)
}

Best Practices Summary

  1. Divide Progressively: Only create the time divisions you need
  2. Use Memoization: Cache expensive calculations
  3. Lazy Load: Load data on-demand, especially for large date ranges
  4. Optimize Bundle: Import only what you need
  5. Manage Memory: Clean up unused data and limit cache sizes
  6. Batch Updates: Group reactive updates together
  7. Virtual Rendering: Use virtual scrolling for large lists
  8. Profile Performance: Measure and optimize bottlenecks

See Also

Released under the MIT License.