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.
// 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.
// 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.
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.
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.
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.
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.
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.
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.
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.
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.
<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.
<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.
// 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.
// 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.
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.
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
- Over-reactivity: Making everything reactive when not needed
- Large objects: Deep reactivity on large objects
- Frequent updates: Not batching state updates
- Memory leaks: Not cleaning up subscriptions
- Unnecessary computations: Not using computed properties
- Bundle bloat: Loading all stores upfront
Performance Testing
// 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)
})
})