Why Your App is Slow (Even with Fiber): State Management Explained
Slow state management application? Discover the anti-patterns killing your React performance (cascading re-renders, Prop Drilling, oversized Context) and concrete solutions with Zustand, useMemo, and React Server Components.
Why Your App is Slow (Even with Fiber)? State Management Explained
Your server responds in 50ms. Your connection is fiber. Yet clicking "Add to cart" takes 2 seconds. The culprit isn't your hosting provider — it's your state management.
If you're wondering why slow state management in applications is such a widespread problem, you're in the right place. State management is the invisible architecture that determines how your application stores, distributes, and updates its data. Done well, it gives users a fluid, near-instant experience. Done poorly, it turns every interaction into a cascade of unnecessary re-renders that exhaust the CPU, drain batteries, and drive users away.
This article explains the precise mechanisms behind React performance slowdowns, the most common anti-patterns in real-world projects, and concrete solutions — from Zustand to React Server Components — with commented code examples and reproducible diagnostic steps.
How State Management Causes Cascading Re-renders
To understand why a React application becomes slow, you first need to understand how React decides to re-render a component.
The React Component Lifecycle
React follows a simple model: when state changes, the component that owns that state re-renders, along with all its children, unless you have explicit optimizations in place. This behavior is intentional and correct for small applications. It becomes catastrophic as your component tree grows or state changes frequently.
Imagine an e-commerce application with a naive architecture. You have an App component that holds in its local state the cart, the user, the theme, and filter preferences. Every time the user switches the theme — an action that only concerns visual appearance — React re-renders App, triggering a re-render of all its children: the product list (potentially hundreds of items), the header, the footer, the search component, and more. None of these components need to be recalculated, yet React recalculates all of them.
// simplified example — ❌ Over-centralized state
function App() {
const [cart, setCart] = useState<CartItem[]>([]);
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// Changing the theme re-renders the ENTIRE tree
return (
<ThemeContext.Provider value={theme}>
<Header user={user} />
<ProductList /> {/* Unnecessary re-render */}
<Cart cart={cart} /> {/* Unnecessary re-render */}
</ThemeContext.Provider>
);
}
The question isn't "is React slow?" — React is extremely fast at virtual rendering. The question is "how much unnecessary work are you asking it to do?"
The Problem With Oversized Context
React Context is beautifully simple to use, but it hides a well-documented performance trap. Every time a Context value changes, all components consuming that Context via useContext re-render — without exception — even if the part of the value they use hasn't changed.
This problem becomes critical when you put everything into a single global Context: user, cart, notifications, theme, preferences. An incoming notification forces a re-render of the component that only displays the user's name. An item added to the cart forces a re-render of the component that only displays the theme.
// simplified example — ❌ Monolithic Context
const AppContext = createContext<{
user: User;
cart: CartItem[];
theme: string;
notifications: Notification[];
}>({ user: null, cart: [], theme: 'light', notifications: [] });
// All consumers re-render if ANYTHING changes
function UserAvatar() {
const { user } = useContext(AppContext);
// Also re-renders when cart or theme changes!
return <img src={user.avatar} />;
}
Without memoization, a theme change re-renders Header, Cart, and Products. With Zustand and targeted selectors, only Header is affected.
Anti-Patterns That Kill Performance
Now that you understand the mechanism, here are the most common anti-patterns found in real-world React projects — the ones that appear in almost every code review.
Anti-Pattern 1: Too Much State in the Global Store
The most widespread mistake is treating the global store as a catch-all. As soon as a piece of data is used in two different places, the natural reflex is to put it in the store. This reflex is dangerous.
Always ask yourself: is this data truly shared between multiple components that cannot communicate directly? If the answer is no, keep it as local state. Local state doesn't propagate re-renders beyond the component that owns it.
The open/closed state of a modal, the current value of a form field being typed, whether an accordion is expanded — all of this belongs to local state. Putting it in a global store is like triggering the entire building's alarm because you turned on your bedside lamp.
Anti-Pattern 2: Denormalized State With Duplications
Denormalization means storing the same piece of data in multiple places for convenience. For example, storing both the product list and a selectedProductDetails object that contains a copy of the selected product.
When the product is updated (price, stock), you need to remember to update both sources. You'll forget one of them — it's inevitable — and your interface will display inconsistent data. This is desynchronized state: the cart shows "3 items" at the top, "Empty" at the bottom. The user doesn't understand what's happening and leaves.
The solution is normalization: store entities once, indexed by ID, and only store the ID where you need to reference an entity.
// simplified example — ✅ Normalized state
interface StoreState {
products: Record<string, Product>; // single source of truth
selectedProductId: string | null; // reference by ID
cartItemIds: string[]; // list of IDs
}
// Reading: always derive from the source of truth
const selectedProduct = store.products[store.selectedProductId];
Anti-Pattern 3: Non-Memoized Selectors in Zustand
Zustand helps avoid unnecessary re-renders through selectors. A selector is a function passed to useStore that extracts only the portion of the store the component needs. Zustand compares the value returned by the selector before and after each store update: if the value hasn't changed, the component doesn't re-render.
But if your selector returns a new object or a new array on every call — even with the same data — Zustand considers the value changed and triggers a re-render.
// simplified example — ❌ Selector that creates a new array every time
function CartSummary() {
// Creates a new array on every store render
const expensiveItems = useCartStore(
(state) => state.items.filter((i) => i.price > 100)
);
return <div>{expensiveItems.length} premium items</div>;
}
// simplified example — ✅ Memoized selector with createSelector (zustand + reselect)
const selectExpensiveItems = createSelector(
[(state: CartState) => state.items],
(items) => items.filter((i) => i.price > 100)
);
function CartSummary() {
const expensiveItems = useCartStore(selectExpensiveItems);
return <div>{expensiveItems.length} premium items</div>;
}
Prop Drilling: data traverses the entire hierarchy unnecessarily. Zustand: each component subscribes directly to the store without intermediaries.
Poorly managed global state triggers 100% re-renders. With Zustand and targeted selectors, only the actually affected components re-render.
React Context vs Zustand vs Redux: Which Tool to Choose?
The question isn't "which tool is best?" but "which tool fits my use case?" Here's how to reason through it.
React Context is perfect for data that changes rarely and needs to be accessible everywhere: theme, language, logged-in user. If the value changes more than once per second, avoid Context — its cascading re-renders cannot be optimized without significant architectural complexity.
Zustand offers the best balance between simplicity and performance for frequently updated client state. Its API is minimal, its memory footprint is small, and its selectors allow surgical subscriptions. For an e-commerce cart, real-time notifications, or an interface with many interactive states, Zustand is the natural choice.
Redux Toolkit remains relevant for very large applications that benefit from imposed structure, time-travel debugging, and the middleware ecosystem. For most medium-sized projects, its complexity-to-benefit ratio is unfavorable compared to Zustand.
TanStack Query (React Query) deserves special mention: it doesn't manage client state, but server state — data fetched from an API. For this use case, it's unbeatable: smart caching, request deduplication, automatic revalidation. Don't duplicate in Zustand what you're fetching from your API — let React Query handle it.
When to use local state, lift state to a common parent, or switch to a global store? This diagram guides your architecture decisions.
useMemo, useCallback, and Zustand Selectors: Practical Guide
These three tools serve the same goal — avoiding unnecessary computations or re-renders — but in different contexts.
useMemo: Memoizing Expensive Calculations
useMemo is useful when a component needs to compute a derived value from data that changes infrequently, but the computation itself is expensive. The typical case: filtering or sorting a list of thousands of items on every render.
// simplified example
function ProductList({ products, searchQuery }: Props) {
// Without useMemo: recomputes on every re-render of the parent component
const filteredProducts = useMemo(
() =>
products.filter(
(p) =>
p.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
p.stock > 0
),
[products, searchQuery] // Only recomputes when these deps change
);
return filteredProducts.map((p) => <ProductCard key={p.id} product={p} />);
}
Note: useMemo has a cost. It allocates memory to store the memoized result and consumes CPU to compare dependencies. Don't add useMemo everywhere — reserve it for calculations that genuinely take time or are called very frequently.
useCallback: Stabilizing Function References
useCallback memoizes a function reference. Its primary utility is preventing a child component wrapped in React.memo from re-rendering because of a new function reference passed as a prop.
// simplified example
const handleAddToCart = useCallback(
(productId: string) => {
cartStore.addItem(productId);
},
[] // Stable reference — never changes
);
// ProductCard is memoized: only re-renders if its props change
const ProductCard = React.memo(({ product, onAdd }: ProductCardProps) => {
return <button onClick={() => onAdd(product.id)}>{product.name}</button>;
});
Zustand Selectors: Surgical Subscriptions
With Zustand, the golden rule is to only extract what you need. A component that only needs the number of cart items should not subscribe to the full items list.
// simplified example — ✅ Granular selector
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
}
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
}));
// Only re-renders if the item COUNT changes
function CartBadge() {
const count = useCartStore((state) => state.items.length);
return <span className="badge">{count}</span>;
}
// Only re-renders if the items themselves change (shallow compare)
function CartDrawer() {
const items = useCartStore((state) => state.items, shallow);
return <ul>{items.map((i) => <CartItem key={i.id} item={i} />)}</ul>;
}
Profiling and Diagnostics: How to Detect State Management Bottlenecks
Before optimizing anything, you need to measure. Optimizing without profiling is like fixing a car without looking at the engine.
React DevTools Profiler
React DevTools is the essential tool for re-render diagnostics. It installs as a browser extension (Chrome, Firefox) and adds a "Profiler" tab to the DevTools.
Procedure:
- Open the Profiler tab in React DevTools.
- Click the record button (red circle).
- Reproduce the interaction you suspect is slow.
- Stop recording.
The Profiler displays a "flamegraph" of renders. Each bar represents a component. Color indicates render duration (green = fast, red = slow). Enable "Highlight updates" mode in DevTools settings: components that re-render are surrounded by a color flash in real time — this is often enough to immediately identify components re-rendering for no reason.
Look primarily for Wasted Renders: components that re-render with the same props and state. This is always a sign of a missing optimization.
Chrome DevTools Performance
For deeper issues — Long Tasks blocking the main thread, expensive JavaScript calculations, GC pressure — use Chrome DevTools' Performance tab.
Procedure:
- Open Chrome DevTools → Performance tab.
- Enable "CPU: 4x slowdown" to simulate a mobile device and amplify issues.
- Click Record, reproduce the interaction, stop.
- Look for Long Tasks (red bars in the "Main" track): any task exceeding 50ms blocks the main thread and causes perceptible jank.
In the detailed view, you can see the JavaScript call tree and identify which function is consuming time. If you see React component names in the flamegraph, you've found your suspects.
Step-by-step guide: from identifying the problem via React DevTools to the appropriate solution (Zustand, useMemo, React Query, RSC).
Real Case: The GPS Delivery Application
Here's a concrete example of a state management problem we encountered on a delivery application. The driver updates their GPS position on the map in real time — the position is updated several times per second via WebSocket.
The initial architecture stored the GPS position in the application's global state, at the same level as the cart, active orders, and user information. Result: with every GPS update, the entire interface re-rendered — the order list, the header, the details panel. The application became unusable after a few minutes of intensive use.
The fix:
// simplified example — ✅ Isolated store for GPS position
const useGPSStore = create<GPSStore>((set) => ({
position: { lat: 0, lng: 0 },
updatePosition: (pos) => set({ position: pos }),
}));
// Only MapPin subscribes to the GPS store
function MapPin() {
const position = useGPSStore((state) => state.position);
return <Marker position={position} />;
}
// OrderList doesn't know the GPS position exists — no re-renders
function OrderList() {
const orders = useOrderStore((state) => state.orders);
return <ul>{orders.map((o) => <OrderCard key={o.id} order={o} />)}</ul>;
}
By isolating the GPS position in its own Zustand store, only the MapPin component receives updates. The rest of the application sleeps peacefully at 60 FPS. For more on web performance optimization from the loading side, check out our guide on dividing loading time by 10.
React Server Components: The Radical Solution to Eliminate the Problem at the Source
React Server Components (RSC), available in Next.js since version 13 and mature in Next.js 15/16, represent a paradigm shift for addressing state management-related slowdowns.
The idea is simple: if a component doesn't need interactivity (no useState, no user events), why send it to the client? RSCs are rendered entirely on the server, their JavaScript code is never sent to the browser, and they don't participate in the client-side state management tree.
For a blog article, a product page with static information, a list of categories — all of these can become Server Components. You reduce your JavaScript bundle size, the amount of client state to manage, and mechanically the number of possible re-renders.
// simplified example — RSC: server rendering, zero client JS
// app/products/page.tsx — Server Component by default in Next.js App Router
async function ProductsPage() {
// Direct server-side fetch, no useEffect, no state
const products = await db.products.findMany({ where: { active: true } });
return (
<main>
<h1>Our Products</h1>
{/* Server Component: zero JS sent to client */}
<ProductGrid products={products} />
{/* Client Component: only the cart needs interactivity */}
<CartButton /> {/* 'use client' only here */}
</main>
);
}
In Next.js 16.2, improvements to Partial Pre-Rendering allow even finer granularity: some parts of the page are served statically (cached at the edge), others are rendered dynamically, all within the same component. Our article on Partial Pre-Rendering in Next.js details this approach. If you're already on Next.js and want to understand the latest changes, also read what Next.js 16.2 changes concretely in 2026.
Step-by-Step Guide: Fixing a Slow Application in 5 Steps
Here's the concrete procedure we apply during our performance audits.
Step 1 — Profile before optimizing. Open React DevTools Profiler, enable "Highlight updates," and reproduce the problematic interaction. Note the 3 components that re-render most frequently in unexpected ways.
Step 2 — Identify the state source. For each problematic component, trace back the chain: is this component consuming a Context? An entire Zustand store? Receiving props that change too often? Find the "origin point" of the re-render.
Step 3 — Isolate and partition state. Split your stores into coherent, independent domains. A cart store, a user store, a UI store (modals, toasts, theme). Each store should cover only one functional domain. Components subscribe only to what they need.
Step 4 — Add selectors. Replace full store access with granular selectors. If you have useStore((state) => state) anywhere in your code, that's a guaranteed performance bug — the component re-renders on any store change, regardless of which part was modified.
Step 5 — Migrate static components to Server Components. Identify components that have no interactivity and don't need client state. Transform them into Server Components to remove their JavaScript from the bundle. This single change can significantly reduce the client state surface to manage.
If your application suffers from broader technical debt problems that complicate these refactorings, our article on why an application costs 5x more to maintain with technical debt will give you a framework for prioritizing the necessary refactorings.
The Truth About AI and State Management
Generative AI tools (GitHub Copilot, Cursor, Claude) significantly accelerate React code writing. But they have a documented tendency to generate Prop Drilling — it's the simplest solution to infer contextually, and large language models optimize for local simplicity, not global architecture.
If you review AI-generated code without understanding the performance implications of state management, you'll accumulate technical debt at high speed. AI can write correct, functional code that is architecturally disastrous for performance.
The rule is simple: AI writes the code, you validate the architecture. If you see Prop Drilling more than two levels deep, or state placed in a global Context that changes frequently, that's a warning signal requiring your human judgment.
Conclusion: From Laggy App to Fluid App
A slow state management application is not inevitable. The causes are well-known, the tools to fix them are mature, and performance gains are often spectacular — without rewriting your entire application.
Remember these priorities:
- Measure first with React DevTools Profiler before optimizing anything.
- Isolate your stores by functional domain and use granular selectors in Zustand.
- Normalize your data to avoid desynchronization and duplications.
- Memoize wisely with
useMemoanduseCallback— not everywhere, only where justified. - Offload to the server with React Server Components everything that doesn't need client interactivity.
A fluid application like Instagram or Uber isn't the result of powerful hardware — it's the product of rigorous state management architecture built on these principles.
FAQ — State Management and React Performance
Why does my React application re-render so much?
Excessive re-renders most commonly come from an oversized global state (non-partitioned Context or store), non-memoized selectors, or props that recreate objects/functions on every render. Use React DevTools Profiler to identify "flashing" components and add targeted Zustand selectors or React.memo to isolate them.
What is the difference between React Context and Zustand?
React Context is a native mechanism that re-renders all consumers whenever its value changes. Zustand is an external library that lets you subscribe only to a specific slice of the store via selectors, avoiding re-renders of unrelated components. For frequently updated states (cart, GPS position), Zustand is significantly more performant.
When should I use useMemo versus useCallback?
useMemo memoizes the result of an expensive calculation — for example, filtering or sorting a list of thousands of items. useCallback memoizes a function reference to prevent a child component wrapped in React.memo from re-rendering due to a new function reference. Don't add these hooks everywhere: they have a memory cost and are only beneficial when the dependency changes less often than the parent component re-renders.
Do React Server Components solve state management performance issues?
Partially. RSCs render server-side any component that doesn't need interactive state, drastically reducing the JavaScript sent to the client. They don't replace Zustand or Context for shared client state, but they eliminate the problem at the root for static or near-static parts of the interface.
How do I profile a React application to find performance bottlenecks?
Open React DevTools (Profiler tab), click "Record", reproduce the slow interaction, then stop recording. Colored bars show re-rendered components and their duration. Complement with Chrome DevTools (Performance tab) to visualize the main thread, Long Tasks, and JavaScript call flames. Prioritize components that re-render for no reason during an unrelated action.
Additional Resources:
Complete Guide: Pro App Development Full module on state architecture (React & Flutter): Zustand, Riverpod, Context API, performance optimization, advanced debugging. Access the Complete Guide
Is your app fluid or does it lag? Share your context in the comments — application type, stack, observed symptoms — and we'll tell you where to start.
Tags
FAQ
Why does my React application re-render so much?
Excessive re-renders most commonly come from an oversized global state (non-partitioned Context or store), non-memoized selectors, or props that recreate objects/functions on every render. Use React DevTools Profiler to identify "flashing" components and add targeted Zustand selectors or React.memo to isolate them.
What is the difference between React Context and Zustand?
React Context is a native mechanism that re-renders all consumers whenever its value changes. Zustand is an external library that lets you subscribe only to a specific slice of the store via selectors, avoiding re-renders of unrelated components. For frequently updated states (cart, GPS position), Zustand is significantly more performant.
When should I use useMemo versus useCallback?
useMemo memoizes the result of an expensive calculation — for example, filtering or sorting a list of thousands of items. useCallback memoizes a function reference to prevent a child component wrapped in React.memo from re-rendering due to a new function reference. Do not add these hooks everywhere: they have a memory cost and are only beneficial when the dependency changes less often than the parent component re-renders.
Do React Server Components solve state management performance issues?
Partially. RSCs render server-side any component that doesn't need interactive state, drastically reducing the JavaScript sent to the client. They don't replace Zustand or Context for shared client state, but they eliminate the problem at the root for static or near-static parts of the interface.
How do I profile a React application to find performance bottlenecks?
Open React DevTools (Profiler tab), click "Record", reproduce the slow interaction, then stop recording. Colored bars show re-rendered components and their duration. Complement with Chrome DevTools (Performance tab) to visualize the main thread, Long Tasks, and JavaScript call flames. Prioritize components that re-render for no reason during an unrelated action.
Ready to implement this?
Book a free 30-min strategy call with our experts
We'll analyze your situation and propose a concrete action plan.

William Aklamavo
Web development and automation expert, passionate about technological innovation and digital entrepreneurship.
