Redux is excellent at giving you control over application state. It is also where a growing React app quietly goes slow. The state gets bigger, components re-render when they should not, and one careless update recalculates work the user never asked for. I have debugged and fixed this pattern in real production apps, and the fixes are not exotic. They come down to keeping the state small, the renders precise, and the derived data memoized.
The short version
Redux performance problems almost always trace to three things: too much in the store, components re-rendering on state they do not use, and selectors recomputing on every render. Fix them by storing only what you need, normalizing nested data, and using memoized selectors so a component re-renders only when the data it actually reads changes. Do that and Redux scales with the app instead of dragging it down. The rest of this post is each fix and when to reach for it.
The three problems that actually slow Redux down
Too much in the store
The store is not a database. When you keep large lists, full server responses, or data you only need once in Redux, every update has to walk a bigger object, and memory grows with it. Keep the store minimal. Store the data the UI genuinely needs to share, and let pagination or infinite scrolling cap how much lives in memory at any one time. If a piece of data is only used by one component and never shared, it probably does not belong in Redux at all.
Re-renders on state a component does not use
This is the most common Redux performance bug. By default, a component connected to the store can re-render whenever the store changes, even if the slice it cares about did not. Multiply that across a large component tree and you are doing constant work for nothing. The fix is to make each component subscribe precisely to the data it reads, and nothing more, so an unrelated update does not wake it up. This same discipline of isolating what changes is what lets teams scale a micro frontend architecture to millions of users without losing velocity: contain change so it does not ripple across the whole app.
Selectors that recompute every render
When you derive data from state, filtering a list, joining two slices, computing a total, a naive selector runs that computation on every single render, even when its inputs have not changed. For a heavy derivation on a large list, that is pure waste. Memoization solves it.
Normalize nested state
Deeply nested state is slow to update because Redux requires immutable updates, which means shallow-copying every level on the way down to the change. The deeper the nesting, the more copying per update.
So flatten it. Store entities in a keyed, relational shape instead of nested objects.
// Normalized state for users and posts
{
users: {
1: { id: 1, name: "John" },
2: { id: 2, name: "Jane" }
},
posts: {
101: { id: 101, title: "Redux Best Practices", userId: 1 }
}
}
Now updating one user touches one shallow object instead of rewriting a deep tree, and looking anything up is a direct key access. Normalization speeds up updates and makes the state far easier to maintain as it grows. A library like normalizr can do the flattening for you.
Memoize derived data with Reselect
Reselect lets you build selectors that only recompute when their inputs change. Between renders where the inputs are identical, the selector returns the previous result instead of recalculating.
import { createSelector } from "reselect";
const selectUsers = state => state.users;
const selectPosts = state => state.posts;
const selectPostsByUser = createSelector(
[selectUsers, selectPosts],
(users, posts) => posts.filter(post => post.userId === users.id)
);
The filtering only runs when users or posts actually changes. Every other render reuses the memoized result. On a large list with an expensive derivation, this is often the single biggest win available, and it is a few lines of code.
Keep side effects out of the render path
Asynchronous work, API calls, sequencing, retries, does not belong tangled into components or reducers. Middleware like redux-thunk or redux-saga moves that logic into a clean, testable layer, which keeps your actions and reducers simple and fast. Simple reducers are fast reducers. The less your state-update path has to do, the less there is to slow down.
Measure before and after
Do not guess at Redux performance. Measure it. The React Profiler shows you which components are re-rendering and how often, which makes wasted renders obvious instead of theoretical. Redux DevTools let you watch each action and the state change it causes, though on a large app you should limit logged actions or disable state snapshots so the tooling itself does not become the bottleneck. Fix what the profiler shows you, then measure again. The point is to spend effort where the renders actually are, not where you assume they are. State updates are only one part of front-end performance; the same evidence-first approach applies to the whole picture, from bundle size to network, which I cover in this guide to improving website speed and the role of tree shaking in your JavaScript bundle.
Frequently asked questions
Why does my Redux app re-render so much? Usually because components subscribe to more of the store than they use, so an unrelated update still triggers them. Make each component select precisely the slice it reads, and use memoized selectors so it only re-renders when that specific data changes.
Should I store everything in Redux? No. The store is for state the UI needs to share. Large lists, one-off data, and anything used by a single component usually do not belong there. Keeping the store minimal reduces memory and makes every update cheaper.
What does normalizing Redux state actually fix? Deeply nested state forces Redux to shallow-copy every level on each immutable update, which is slow. Normalizing into a flat, keyed shape means an update touches one small object and lookups are direct key access. It speeds up updates and simplifies maintenance.
When should I use Reselect? Whenever you derive data from state, such as filtering, joining slices, or computing totals. Reselect memoizes the result so the computation only runs when its inputs change, instead of on every render. On large lists with expensive derivations it is often the biggest single performance win.
The takeaway
Redux does not get slow on its own. It gets slow when the store holds too much, components re-render on data they do not use, and selectors recompute work that has not changed. Keep the state minimal and normalized, subscribe precisely, memoize your derived data, and measure with the profiler so you fix the renders that are real. Done that way, Redux stays fast as the app grows.