Skip to content

Performance Optimization ​

This guide covers various techniques to optimize the performance of your Pinia stores and Vue applications.

Store Optimization ​

Lazy Loading Stores ​

Load stores only when needed to reduce initial bundle size and improve startup performance.

ts
// stores/index.ts
export const useUserStore = () => import('./user').then(m => m.useUserStore)
export const useProductStore = () => import('./product').then(m => m.useProductStore)

// In component
<script setup>
const loadUserStore = async () => {
  const { useUserStore } = await import('@/stores/user')
  return useUserStore()
}

const userStore = await loadUserStore()
</script>

Store Splitting ​

Split large stores into smaller, focused stores to improve maintainability and performance.

ts
// Instead of one large store
const useLargeStore = defineStore('large', () => {
  const users = ref([])
  const products = ref([])
  const orders = ref([])
  // ... many more states
})

// Split into focused stores
const useUserStore = defineStore('user', () => {
  const users = ref([])
  const currentUser = ref(null)
  
  const fetchUsers = async () => {
    // Implementation
  }
  
  return { users, currentUser, fetchUsers }
})

const useProductStore = defineStore('product', () => {
  const products = ref([])
  const categories = ref([])
  
  const fetchProducts = async () => {
    // Implementation
  }
  
  return { products, categories, fetchProducts }
})

State Optimization ​

Computed Properties ​

Use computed properties for derived state to avoid unnecessary recalculations.

ts
const useProductStore = defineStore('product', () => {
  const products = ref([])
  const filters = ref({ category: '', priceRange: [0, 1000] })
  
  // Efficient computed property
  const filteredProducts = computed(() => {
    return products.value.filter(product => {
      const matchesCategory = !filters.value.category || 
        product.category === filters.value.category
      const matchesPrice = product.price >= filters.value.priceRange[0] && 
        product.price <= filters.value.priceRange[1]
      
      return matchesCategory && matchesPrice
    })
  })
  
  // Expensive operations should be computed
  const productStats = computed(() => {
    const total = products.value.length
    const avgPrice = products.value.reduce((sum, p) => sum + p.price, 0) / total
    const categories = [...new Set(products.value.map(p => p.category))]
    
    return { total, avgPrice, categories }
  })
  
  return {
    products,
    filters,
    filteredProducts,
    productStats
  }
})

Shallow Reactivity ​

Use shallowRef for large objects that don't need deep reactivity.

ts
import { shallowRef, triggerRef } from 'vue'

const useDataStore = defineStore('data', () => {
  // For large datasets that change as a whole
  const largeDataset = shallowRef([])
  
  const updateDataset = (newData) => {
    largeDataset.value = newData
    triggerRef(largeDataset) // Manually trigger reactivity
  }
  
  return { largeDataset, updateDataset }
})

Selective Reactivity ​

Make only necessary properties reactive.

ts
const useConfigStore = defineStore('config', () => {
  // Reactive for UI updates
  const theme = ref('light')
  const language = ref('en')
  
  // Non-reactive for static configuration
  const apiEndpoints = {
    users: '/api/users',
    products: '/api/products'
  }
  
  // Reactive only when needed
  const debugMode = ref(false)
  
  return {
    theme,
    language,
    apiEndpoints,
    debugMode
  }
})

Action Optimization ​

Debounced Actions ​

Debounce frequent actions to reduce API calls.

ts
import { debounce } from 'lodash-es'

const useSearchStore = defineStore('search', () => {
  const query = ref('')
  const results = ref([])
  const loading = ref(false)
  
  const searchAPI = async (searchQuery: string) => {
    loading.value = true
    try {
      const response = await fetch(`/api/search?q=${searchQuery}`)
      results.value = await response.json()
    } finally {
      loading.value = false
    }
  }
  
  // Debounce search to avoid excessive API calls
  const debouncedSearch = debounce(searchAPI, 300)
  
  const search = (searchQuery: string) => {
    query.value = searchQuery
    if (searchQuery.trim()) {
      debouncedSearch(searchQuery)
    } else {
      results.value = []
    }
  }
  
  return {
    query,
    results,
    loading,
    search
  }
})

Batch Operations ​

Batch multiple operations to reduce reactivity overhead.

ts
const useCartStore = defineStore('cart', () => {
  const items = ref([])
  
  // Inefficient: triggers reactivity for each item
  const addMultipleItemsInefficient = (newItems) => {
    newItems.forEach(item => {
      items.value.push(item)
    })
  }
  
  // Efficient: single reactivity trigger
  const addMultipleItems = (newItems) => {
    items.value = [...items.value, ...newItems]
  }
  
  // Using $patch for multiple updates
  const updateCart = (updates) => {
    // Batch multiple state updates
    $patch((state) => {
      if (updates.items) state.items = updates.items
      if (updates.total) state.total = updates.total
      if (updates.discount) state.discount = updates.discount
    })
  }
  
  return {
    items,
    addMultipleItems,
    updateCart
  }
})

Async Action Optimization ​

Optimize async actions with caching and request deduplication.

ts
const useUserStore = defineStore('user', () => {
  const users = ref(new Map())
  const loading = ref(new Set())
  
  // Cache and deduplicate requests
  const fetchUser = async (userId: string) => {
    // Return cached user if available
    if (users.value.has(userId)) {
      return users.value.get(userId)
    }
    
    // Prevent duplicate requests
    if (loading.value.has(userId)) {
      // Wait for ongoing request
      while (loading.value.has(userId)) {
        await new Promise(resolve => setTimeout(resolve, 10))
      }
      return users.value.get(userId)
    }
    
    loading.value.add(userId)
    
    try {
      const response = await fetch(`/api/users/${userId}`)
      const user = await response.json()
      users.value.set(userId, user)
      return user
    } finally {
      loading.value.delete(userId)
    }
  }
  
  return {
    users,
    fetchUser
  }
})

Memory Management ​

Store Cleanup ​

Clean up stores when they're no longer needed.

ts
const useTemporaryStore = defineStore('temporary', () => {
  const data = ref([])
  const subscriptions = ref([])
  
  const cleanup = () => {
    // Clear data
    data.value = []
    
    // Unsubscribe from external sources
    subscriptions.value.forEach(unsubscribe => unsubscribe())
    subscriptions.value = []
  }
  
  // Auto-cleanup on unmount
  onUnmounted(() => {
    cleanup()
  })
  
  return {
    data,
    cleanup
  }
})

Weak References ​

Use weak references for temporary data.

ts
const useCacheStore = defineStore('cache', () => {
  const cache = new WeakMap()
  const tempData = new Map()
  
  const setTempData = (key: string, value: any, ttl: number = 5000) => {
    tempData.set(key, value)
    
    // Auto-cleanup after TTL
    setTimeout(() => {
      tempData.delete(key)
    }, ttl)
  }
  
  return {
    cache,
    tempData,
    setTempData
  }
})

Component Integration ​

Selective Store Usage ​

Only use the parts of the store you need in components.

vue
<script setup>
// Instead of using entire store
const store = useProductStore()

// Use only what you need
const { products, loading } = storeToRefs(useProductStore())
const { fetchProducts } = useProductStore()

// Or use computed for specific data
const featuredProducts = computed(() => 
  useProductStore().products.filter(p => p.featured)
)
</script>

Conditional Store Loading ​

Load stores conditionally based on component needs.

vue
<script setup>
const props = defineProps<{
  showUserData: boolean
  showProductData: boolean
}>()

// Conditional store loading
const userStore = props.showUserData ? useUserStore() : null
const productStore = props.showProductData ? useProductStore() : null

// Or use dynamic imports
const loadStoreData = async () => {
  if (props.showUserData) {
    const { useUserStore } = await import('@/stores/user')
    const userStore = useUserStore()
    await userStore.fetchUsers()
  }
}
</script>

Bundle Optimization ​

Tree Shaking ​

Structure stores to enable effective tree shaking.

ts
// stores/user/index.ts
export { useUserStore } from './store'
export { userHelpers } from './helpers'
export type { User, UserState } from './types'

// stores/user/store.ts
export const useUserStore = defineStore('user', () => {
  // Store implementation
})

// stores/user/helpers.ts
export const userHelpers = {
  formatUserName: (user) => `${user.firstName} ${user.lastName}`,
  isUserActive: (user) => user.status === 'active'
}

Code Splitting ​

Split store code by feature.

ts
// stores/user/actions.ts
export const createUserActions = () => {
  const login = async (credentials) => {
    // Implementation
  }
  
  const logout = () => {
    // Implementation
  }
  
  return { login, logout }
}

// stores/user/getters.ts
export const createUserGetters = (state) => {
  const isLoggedIn = computed(() => !!state.user.value)
  const fullName = computed(() => 
    state.user.value ? `${state.user.value.firstName} ${state.user.value.lastName}` : ''
  )
  
  return { isLoggedIn, fullName }
}

// stores/user/index.ts
export const useUserStore = defineStore('user', () => {
  const state = {
    user: ref(null)
  }
  
  const actions = createUserActions()
  const getters = createUserGetters(state)
  
  return {
    ...state,
    ...actions,
    ...getters
  }
})

Performance Monitoring ​

Store Performance Tracking ​

Monitor store performance in development.

ts
const usePerformanceStore = defineStore('performance', () => {
  const metrics = ref(new Map())
  
  const trackAction = (actionName: string, fn: Function) => {
    return async (...args: any[]) => {
      const start = performance.now()
      
      try {
        const result = await fn(...args)
        const duration = performance.now() - start
        
        if (import.meta.env.DEV) {
          console.log(`Action ${actionName} took ${duration.toFixed(2)}ms`)
          
          const current = metrics.value.get(actionName) || []
          current.push(duration)
          metrics.value.set(actionName, current)
        }
        
        return result
      } catch (error) {
        const duration = performance.now() - start
        console.error(`Action ${actionName} failed after ${duration.toFixed(2)}ms`, error)
        throw error
      }
    }
  }
  
  const getMetrics = () => {
    const summary = new Map()
    
    metrics.value.forEach((durations, actionName) => {
      const avg = durations.reduce((a, b) => a + b, 0) / durations.length
      const min = Math.min(...durations)
      const max = Math.max(...durations)
      
      summary.set(actionName, { avg, min, max, count: durations.length })
    })
    
    return summary
  }
  
  return {
    trackAction,
    getMetrics
  }
})

Memory Usage Monitoring ​

Monitor memory usage of stores.

ts
const useMemoryMonitor = defineStore('memoryMonitor', () => {
  const memoryUsage = ref([])
  
  const recordMemoryUsage = () => {
    if ('memory' in performance) {
      const memory = (performance as any).memory
      memoryUsage.value.push({
        timestamp: Date.now(),
        used: memory.usedJSHeapSize,
        total: memory.totalJSHeapSize,
        limit: memory.jsHeapSizeLimit
      })
      
      // Keep only last 100 records
      if (memoryUsage.value.length > 100) {
        memoryUsage.value = memoryUsage.value.slice(-100)
      }
    }
  }
  
  // Record memory usage every 5 seconds in development
  if (import.meta.env.DEV) {
    setInterval(recordMemoryUsage, 5000)
  }
  
  return {
    memoryUsage,
    recordMemoryUsage
  }
})

Best Practices ​

Performance Checklist ​

  • ✅ Use computed properties for derived state
  • ✅ Implement lazy loading for large stores
  • ✅ Debounce frequent actions
  • ✅ Batch state updates when possible
  • ✅ Clean up stores and subscriptions
  • ✅ Use selective reactivity
  • ✅ Implement caching for expensive operations
  • ✅ Monitor performance in development
  • ✅ Split large stores into smaller ones
  • ✅ Use tree shaking and code splitting

Common Performance Pitfalls ​

  1. Over-reactivity: Making everything reactive when not needed
  2. Large objects: Deep reactivity on large objects
  3. Frequent updates: Not batching state updates
  4. Memory leaks: Not cleaning up subscriptions
  5. Unnecessary computations: Not using computed properties
  6. Bundle bloat: Loading all stores upfront

Performance Testing ​

ts
// performance.test.ts
import { describe, it, expect } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useProductStore } from '@/stores/product'

describe('Store Performance', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('should handle large datasets efficiently', () => {
    const store = useProductStore()
    const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `Product ${i}`,
      price: Math.random() * 100
    }))
    
    const start = performance.now()
    store.setProducts(largeDataset)
    const duration = performance.now() - start
    
    expect(duration).toBeLessThan(100) // Should complete in under 100ms
  })
  
  it('should efficiently filter large datasets', () => {
    const store = useProductStore()
    // Setup large dataset
    
    const start = performance.now()
    const filtered = store.filteredProducts
    const duration = performance.now() - start
    
    expect(duration).toBeLessThan(50)
  })
})

Released under the MIT License.