Practical Performance Tuning with SoulState
SoulState is designed for high performance out of the box, but understanding its optimization mechanisms helps you build even faster applications. This guide covers practical techniques to ensure your SoulState apps run at peak efficiency.
1. Optimize Selector Usage
Selectors are the foundation of SoulState's performance. Proper usage ensures components only re-render when necessary.
Select Minimal Data
Always extract the smallest possible slice of state needed by your component.
// ❌ Anti-pattern: Re-renders for any user change
// const user = useStore(userStore, state => state.user);
// ✅ Best Practice: Re-renders only when name changes
const userName = useStore(userStore, state => state.user.name);
// ✅ Better: For user profile display
const { name, avatar } = useStore(userStore, state => ({
name: state.user.name,
avatar: state.user.avatar
}));
Use shallow for Object Selections
When selectors return new objects, use shallow equality to prevent unnecessary re-renders.
import { useStore } from 'soulstate/react';
import { shallow } from 'soulstate/utils';
// ❌ Anti-pattern: Always re-renders (new object every time)
const userData = useStore(userStore, state => ({
name: state.user.name,
email: state.user.email
}));
// ✅ Best Practice: Only re-renders when name or email actually change
const userData = useStore(
userStore,
state => ({
name: state.user.name,
email: state.user.email
}),
shallow // Uses shallow comparison
);
Memoize Expensive Selectors
For computationally intensive operations, memoize selectors to avoid redundant calculations.
// utils/memoize.ts
export function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map();
return ((...args: any[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
// stores/productSelectors.ts
export const productSelectors = {
getExpensiveProducts: memoize((state: ProductState, minPrice: number) => {
// Expensive filtering operation
return Array.from(state.products.values())
.filter(product => product.price >= minPrice)
.sort((a, b) => b.price - a.price);
}),
getProductStats: memoize((state: ProductState) => {
const products = Array.from(state.products.values());
return {
total: products.length,
averagePrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
expensiveCount: products.filter(p => p.price > 100).length
};
})
};
// Component usage
function ProductStats() {
const stats = useStore(productStore, productSelectors.getProductStats);
const expensiveProducts = useStore(
productStore,
state => productSelectors.getExpensiveProducts(state, 100)
);
return <div>Expensive: {expensiveProducts.length}</div>;
}
2. Structural Sharing Optimization
SoulState automatically optimizes state updates by checking for actual changes before creating new objects.
How SoulState's Change Detection Works
// From src/core/store.ts - SoulState's internal optimization
const set = (updater: StateUpdater<T>) => {
const partialState = typeof updater === 'function'
? updater(state)
: updater;
// Check if any value actually changed
let hasChanged = false;
const updatedKeys = Object.keys(partialState);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i] as keyof T;
if (!Object.is(state[key], (partialState as T)[key])) {
hasChanged = true;
break;
}
}
// Only create new object if something actually changed
if (!hasChanged) return;
const nextState = { ...state, ...(partialState as Partial<T>) };
state = nextState;
scheduleNotification();
};
Leverage This Optimization
// ✅ SoulState automatically skips updates when values don't change
userActions.updateProfile = (updates: Partial<UserProfile>) => {
// If updates don't actually change anything, no re-renders occur
userStore.set(state => ({
user: { ...state.user, ...updates }
}));
};
// Example: This won't cause re-renders if user.name is already 'John'
userActions.updateProfile({ name: 'John' });
3. Microtask Batching Benefits
SoulState batches multiple updates using queueMicrotask, ensuring optimal rendering performance.
Automatic Batching in Action
// All these updates result in a single re-render
const formActions = {
submitForm: (formData: FormData) => {
// All set calls in the same microtask are batched
formStore.set({ data: formData });
formStore.set({ submitted: true });
formStore.set({ lastSubmitted: new Date() });
formStore.set({ loading: false });
// Results in ONE re-render with all changes applied
}
};
// Even in event handlers with multiple stores
const handleUserAction = () => {
userStore.set({ lastActivity: new Date() });
analyticsStore.set(state => ({ eventCount: state.eventCount + 1 }));
uiStore.set({ buttonClicked: true });
// Still only one re-render per store
};
4. Optimize Component Architecture
Use Row-Level Components for Lists
// ✅ Good: Row-level subscriptions
function ProductList() {
// Only subscribe to product IDs
const productIds = useStore(
productStore,
state => Array.from(state.products.keys())
);
return (
<div>
{productIds.map(id => (
<ProductRow key={id} productId={id} />
))}
</div>
);
}
// Each row subscribes only to its data
function ProductRow({ productId }: { productId: string }) {
const product = useStore(
productStore,
state => state.products.get(productId)
);
return <div>{product?.name}</div>;
}
Multiple Fine-grained Subscriptions
// ✅ Better than combined object selection
function UserDashboard() {
const name = useStore(userStore, state => state.user.name);
const email = useStore(userStore, state => state.user.email);
const theme = useStore(uiStore, state => state.theme);
// Each subscription updates independently
return (
<div className={theme}>
<h1>{name}</h1>
<p>{email}</p>
</div>
);
}
5. Data Structure Optimization
Use Maps for Large Datasets
// ✅ Good: Maps for efficient lookups and updates
export const productStore = createStore({
products: new Map<string, Product>(), // O(1) lookups
categories: new Set<string>() // O(1) membership checks
});
export const productActions = {
updateProduct: (id: string, updates: Partial<Product>) => {
productStore.set(state => {
const product = state.products.get(id);
if (!product) return state;
const updatedProduct = { ...product, ...updates };
return {
products: new Map(state.products).set(id, updatedProduct)
};
});
},
// Efficient batch updates
updateMultipleProducts: (updates: Array<{ id: string; changes: Partial<Product> }>) => {
productStore.set(state => {
const newProducts = new Map(state.products);
updates.forEach(({ id, changes }) => {
const existing = newProducts.get(id);
if (existing) {
newProducts.set(id, { ...existing, ...changes });
}
});
return { products: newProducts };
});
}
};
6. Memory Optimization
Avoid Storing Large Objects
// ❌ Avoid: Storing large binary data in stores
export const fileStore = createStore({
largeFile: null as ArrayBuffer | null // Can cause memory issues
});
// ✅ Better: Store references and manage large data separately
export const fileStore = createStore({
fileMetadata: null as { id: string; size: number; url: string } | null,
// Store metadata only, not the actual file content
});
Clean Up Subscriptions
// ✅ Proper subscription cleanup
function RealTimeData({ dataId }: { dataId: string }) {
useEffect(() => {
const unsubscribe = dataStore.subscribe(
state => state.realTimeData.get(dataId),
(newData, oldData) => {
// Handle real-time updates
}
);
return unsubscribe; // Important for memory management
}, [dataId]);
return <div>Real-time component</div>;
}
7. Performance Monitoring
Use React DevTools Profiler
// Look for unnecessary re-renders in React DevTools
function UserProfile() {
const user = useStore(userStore, state => state.user);
const theme = useStore(uiStore, state => state.theme);
// In DevTools Profiler, check if this component
// re-renders when only theme changes but user doesn't
return (
<div className={theme}>
<h1>{user.name}</h1>
</div>
);
}
Custom Performance Monitoring
// Add performance logging for critical actions
export const monitoredActions = {
criticalOperation: async (data: any) => {
const startTime = performance.now();
try {
await someCriticalAction(data);
} finally {
const duration = performance.now() - startTime;
if (duration > 100) { // Log if operation takes >100ms
console.warn(`Critical operation took ${duration.toFixed(2)}ms`);
}
}
}
};
8. Advanced Optimization Patterns
Lazy Store Initialization
// Initialize heavy stores only when needed
let heavyStore: ReturnType<typeof createStore> | null = null;
export const getHeavyStore = () => {
if (!heavyStore) {
heavyStore = createStore({
// Heavy initial state or computations
largeDataset: initializeLargeDataset()
});
}
return heavyStore;
};
// Usage in components
function HeavyComponent() {
const store = getHeavyStore();
const data = useStore(store, state => state.largeDataset);
return <div>{data.length} items</div>;
}
Selective Store Loading
// Load store data only when component mounts
function FeatureComponent() {
const [store] = useState(() => createStore(featureInitialState));
useEffect(() => {
// Load feature-specific data
featureActions.loadData();
}, []);
const data = useStore(store, state => state.data);
return <div>{data}</div>;
}
Performance Checklist
- ✅ Use precise selectors to minimize re-renders
- ✅ Apply
shallowequality for object selections - ✅ Memoize expensive selector computations
- ✅ Leverage automatic microtask batching
- ✅ Use Maps for large, frequently updated datasets
- ✅ Implement row-level components for lists
- ✅ Monitor performance with React DevTools
- ✅ Clean up subscriptions properly
Pro Performance Tips
- Start with performance in mind - don't optimize prematurely, but design for it
- Use the browser's Performance tab to identify real bottlenecks
- Test with large datasets to ensure your app scales well
- Consider virtual scrolling for very large lists
- Use SoulState's built-in optimizations before adding complex caching