Redux Toolkit Performance Tips
How to keep your Redux app fast and responsive.
Memoized Selectors
Use createSelector to avoid unnecessary re-renders:
import { createSelector } from '@reduxjs/toolkit'
// Bad - runs every time
const selectExpensiveData = (state) => {
return state.items.filter(item => item.active).map(item => {
// expensive calculation
return { ...item, calculated: expensiveFunction(item) }
})
}
// Good - only runs when state.items changes
const selectExpensiveData = createSelector(
[(state) => state.items],
(items) => {
return items.filter(item => item.active).map(item => ({
...item,
calculated: expensiveFunction(item)
}))
}
)
Multiple Selectors
Combine multiple selectors efficiently:
// Multiple inputs
const selectVisibleTodos = createSelector(
[
(state) => state.todos.items,
(state) => state.todos.filter,
(state) => state.todos.searchText,
],
(todos, filter, searchText) => {
return todos
.filter(todo => {
// Filter logic
if (filter === 'completed') return todo.completed
if (filter === 'active') return !todo.completed
return true
})
.filter(todo => {
// Search logic
return todo.text.toLowerCase().includes(searchText.toLowerCase())
})
}
)
Normalize State Shape
Use Entity Adapter for lists:
// Bad - finding items is slow
const state = {
posts: [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
{ id: 3, title: 'Post 3' },
]
}
// To find post with id 2:
const post = state.posts.find(p => p.id === 2) // O(n) time
// Good - fast lookups
const state = {
posts: {
ids: [1, 2, 3],
entities: {
1: { id: 1, title: 'Post 1' },
2: { id: 2, title: 'Post 2' },
3: { id: 3, title: 'Post 3' },
}
}
}
// To find post with id 2:
const post = state.posts.entities[2] // O(1) time
Avoid Large Objects in Actions
Don't put big data in actions:
// Bad - large payload in every action
dispatch({
type: 'posts/updatePost',
payload: {
id: 1,
title: 'New title',
largeData: /* huge object */,
metadata: /* another big object */
}
})
// Good - only send what changed
dispatch({
type: 'posts/updatePost',
payload: {
id: 1,
changes: { title: 'New title' }
}
})
Batching Updates
Group multiple updates together:
import { batch } from 'react-redux'
// Bad - causes multiple re-renders
dispatch(action1())
dispatch(action2())
dispatch(action3())
// Good - only one re-render
batch(() => {
dispatch(action1())
dispatch(action2())
dispatch(action3())
})
Lazy Loading
Split large slices into smaller pieces:
// Load heavy data only when needed
const heavyDataSlice = createSlice({
name: 'heavyData',
initialState: {
data: null,
loaded: false,
},
reducers: {
loadData: (state, action) => {
state.data = action.payload
state.loaded = true
}
}
})
// Component
function HeavyComponent() {
const { data, loaded } = useAppSelector(state => state.heavyData)
const dispatch = useAppDispatch()
useEffect(() => {
if (!loaded) {
// Only load when component mounts
import('./heavyDataProcessor').then(module => {
const processedData = module.processData()
dispatch(loadData(processedData))
})
}
}, [loaded, dispatch])
return loaded ? <DataView data={data} /> : <Loading />
}
Smart Component Optimization
Use React.memo and selective subscriptions:
import { shallowEqual } from 'react-redux'
// Only re-render when specific data changes
const TodoItem = React.memo(({ todoId }) => {
const todo = useAppSelector(
state => state.todos.entities[todoId],
shallowEqual // Shallow comparison instead of reference equality
)
return <div>{todo.text}</div>
})
Avoid Inline Objects
Don't create objects in render:
// Bad - creates new object every render
function Component() {
const data = useAppSelector(state => ({
user: state.user,
posts: state.posts
}))
// Component re-renders every time
}
// Good - stable selector
const selectUserAndPosts = createSelector(
[state => state.user, state => state.posts],
(user, posts) => ({ user, posts })
)
function Component() {
const data = useAppSelector(selectUserAndPosts)
// Only re-renders when user or posts change
}
RTK Query Optimization
Optimize API queries:
const apiSlice = createApi({
// Keep unused data for longer to reduce requests
keepUnusedDataFor: 60,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
// Transform response to keep only needed data
transformResponse: (response) =>
response.map(post => ({
id: post.id,
title: post.title,
// Don't store unnecessary fields
})),
}),
getPost: builder.query({
query: (id) => `/posts/${id}`,
// Don't refetch if data is recent
forceRefetch: ({ currentArg, previousArg }) => {
return currentArg !== previousArg
},
}),
}),
})
State Shape Design
Design state for performance:
// Bad - deeply nested
const state = {
app: {
user: {
profile: {
personal: {
name: 'John',
email: 'john@example.com'
}
}
}
}
}
// Good - flat structure
const state = {
user: {
id: 1,
name: 'John',
email: 'john@example.com'
}
}
Debounced Actions
Prevent too many rapid actions:
import { debounce } from 'lodash'
// Debounce search to avoid too many API calls
const debouncedSearch = debounce((dispatch, query) => {
dispatch(searchPosts(query))
}, 300)
function SearchBox() {
const dispatch = useAppDispatch()
const handleSearch = (query) => {
debouncedSearch(dispatch, query)
}
return <input onChange={(e) => handleSearch(e.target.value)} />
}
Virtual Lists
For huge lists, use virtualization:
import { VariableSizeList } from 'react-window'
function VirtualPostList() {
const posts = useAppSelector(selectAllPosts)
const Row = ({ index, style }) => (
<div style={style}>
<PostItem post={posts[index]} />
</div>
)
return (
<VariableSizeList
height={600}
itemCount={posts.length}
itemSize={() => 80}
>
{Row}
</VariableSizeList>
)
}
Memory Management
Clean up unused data:
const postsSlice = createSlice({
name: 'posts',
initialState: postsAdapter.getInitialState(),
reducers: {
// Clean up old data
cleanupOldPosts: (state) => {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
const recentPosts = Object.values(state.entities)
.filter(post => post.createdAt > oneWeekAgo)
postsAdapter.setAll(state, recentPosts)
}
}
})
DevTools Configuration
Optimize Redux DevTools for production:
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production' && {
// Limit action history to save memory
maxAge: 50,
// Skip logging large actions
predicate: (state, action) => {
return !action.type.includes('bulkUpdate')
}
}
})
Performance Monitoring
Track performance in development:
const performanceMiddleware = (store) => (next) => (action) => {
if (process.env.NODE_ENV === 'development') {
const start = performance.now()
const result = next(action)
const end = performance.now()
if (end - start > 10) { // Warn about slow actions
console.warn(`Slow action: ${action.type} took ${end - start}ms`)
}
return result
}
return next(action)
}
Key Performance Rules
- Normalize data - Use entities instead of arrays
- Memoize selectors - Avoid expensive calculations
- Shallow comparisons - Use shallowEqual when appropriate
- Batch updates - Group related actions
- Split large slices - Keep slices focused
- Optimize subscriptions - Only subscribe to what you need
- Clean up data - Remove old/unused data
- Profile regularly - Use React DevTools and Redux DevTools
Remember: premature optimization is bad, but understanding these patterns helps you build performant apps from the start!