DEV Community

Cover image for My React App Was Slow Until I Did This: Performance Tips for MERN Stack Developers
Prajesh Prajapati
Prajesh Prajapati

Posted on

My React App Was Slow Until I Did This: Performance Tips for MERN Stack Developers

🐒 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]);
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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>
Enter fullscreen mode Exit fullscreen mode

βœ… Solution:

Use useCallback to memoize functions:

const handleDelete = useCallback((id) => {
  // delete logic
}, []);

<button onClick={() => handleDelete(task.id)}>Delete</button>
Enter fullscreen mode Exit fullscreen mode

πŸ” 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

🧳 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" }]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

βœ… Best practice:

{
  users: {
    1: { id: 1, name: "Alice" }
  },
  tasks: {
    1: { id: 1, title: "Fix bug", userId: 1 }
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ 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} />
))}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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)} />
Enter fullscreen mode Exit fullscreen mode

βœ… Good: Debounce the input

import { debounce } from 'lodash';

const debouncedSearch = useMemo(() =>
  debounce((query) => searchTasks(query), 500), []);

<input onChange={(e) => debouncedSearch(e.target.value)} />
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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();
Enter fullscreen mode Exit fullscreen mode

🎯 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, and useMemo
  • 🧠 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)