π’ My React App Was Slow Until I Did Thisβ¦
Performance Optimization Tips for MERN Stack Developers
βWhy is my React app so slow?β
That was me β frustrated with lag, excessive renders, and sluggish performance. As a MERN stack developer, I was so focused on features that I overlooked performance. Let me walk you through 8 concrete steps I took to speed up my app β from React tweaks to MongoDB query optimizations.
π My Stack Setup
I built a full-stack task management app using:
- π§ MongoDB + Mongoose (Database)
- πͺ Express.js (REST API)
- βοΈ React + Redux (Frontend)
- βοΈ Node.js (Server runtime)
Once the project grew to multiple components, filters, and interactions β it became noticeably slower. Thatβs when I dug into performance optimization.
π οΈ Step 1: Identify Rendering Bottlenecks
The first step is always profiling your app.
β What I Did:
I opened Chrome DevTools β Performance Tab and recorded interactions like page load, filtering, and typing in a search box.
β What I Found:
- Components were re-rendering excessively, even when props didnβt change.
- Expensive calculations were running on every render.
π‘ The Fix: Use React.memo
and useMemo
// β
Prevents re-render if props are unchanged
const TaskItem = React.memo(({ task }) => {
return <li>{task.title}</li>;
});
// β
Memoize derived values (expensive calculations)
const highPriorityTasks = useMemo(() => {
return tasks.filter(task => task.priority === "high");
}, [tasks]);
π This drastically reduced re-renders and improved interaction speed.
π§ Step 2: Avoid Anonymous Functions in JSX
Every render creates a new function instance, which triggers re-renders in child components.
β Problem:
<button onClick={() => handleClick(task.id)}>Delete</button>
β Solution:
Use useCallback
to memoize functions:
const handleDelete = useCallback((id) => {
// delete logic
}, []);
<button onClick={() => handleDelete(task.id)}>Delete</button>
π Now handleDelete
has a stable reference and won't trigger unnecessary re-renders.
π’ Step 3: Lazy Load Heavy Components
Don't load everything at once β lazy load pages and heavy components.
import { lazy, Suspense } from "react";
const TaskDetails = lazy(() => import("./TaskDetails"));
<Suspense fallback={<div>Loading...</div>}>
<TaskDetails />
</Suspense>
This reduced my initial bundle size significantly.
π§ Step 4: Code Splitting with React Router
If you're using react-router-dom
, code-split your routes:
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
π§³ This defers route components until needed, speeding up your homepage.
π§± Step 5: Normalize Redux/Context State
I initially had deeply nested objects, which caused re-renders and slow updates.
β Anti-pattern:
{
users: [
{
id: 1,
name: "Alice",
tasks: [{ id: 1, title: "Fix bug" }]
}
]
}
β Best practice:
{
users: {
1: { id: 1, name: "Alice" }
},
tasks: {
1: { id: 1, title: "Fix bug", userId: 1 }
}
}
π¦ Use Redux Toolkit to manage normalized state.
π Step 6: Use Stable Keys in Lists
Always provide unique and stable keys when rendering lists.
{tasks.map(task => (
<TaskItem key={task._id} task={task} />
))}
π This helps React efficiently update only the changed DOM nodes.
π Step 7: Debounce Input/API Calls
When I implemented search, I was firing API requests on every keystroke.
β Bad:
<input onChange={(e) => searchTasks(e.target.value)} />
β Good: Debounce the input
import { debounce } from 'lodash';
const debouncedSearch = useMemo(() =>
debounce((query) => searchTasks(query), 500), []);
<input onChange={(e) => debouncedSearch(e.target.value)} />
π This reduced unnecessary API calls, improving both frontend and backend performance.
π‘ Step 8: Optimize MongoDB Queries
Turns out, not all slowness is caused by React β sometimes your API is slow.
β What I Fixed:
- Added indexes to MongoDB collections
- Used
.lean()
for read queries to skip Mongoose overhead - Avoided large
$or
and$in
queries without indexes
const tasks = await Task.find({ status: 'active' }).lean();
π― Queries became 2β3x faster, which made frontend responses instant.
π Results: Before vs After
Metric | Before Optimization | After Optimization |
---|---|---|
Initial Load Time | ~6.5 sec | ~2.4 sec |
Average Component Renders | ~500+ | <100 |
Search API Calls per second | 15β20 | ~2 |
React Memory Usage | High | Normal |
π§ Final Takeaways
Performance in a MERN app isn't magic β it's a series of small, intentional improvements:
- π« Avoid unnecessary re-renders with
React.memo
,useCallback
, anduseMemo
- π§ Normalize global state
- π’ Lazy load pages and debounce inputs
- βοΈ Optimize backend queries
π Have You Faced This Too?
Let me know in the comments if your React app is slow and you're stuck. I'd love to help you debug or even collaborate on performance tips.
π¬ Follow for More
Iβll be sharing more about MERN stack development, real-world problems, and practical solutions. Hit follow if you found this post helpful!
Thanks for reading β€οΈ
Top comments (0)