This project demonstrates how to handle large datasets (5,000+ items) in React without compromising user experience. It compares a "Naive Implementation" (Laggy) against an "Architected Solution" (Optimized) to visualize the impact of Rendering Performance, State Normalization, and Virtualization.
- Install Dependencies:
npm install
- Run the App:
npm run dev
- Open in Browser:
Navigate to
http://localhost:5173
This app contains two distinct engines. You can toggle between them using the buttons in the UI.
- State: Stores data as a giant Array (
Task[]). Updates require.map()which replaces the entire array reference. - Rendering: Renders 5,000 DOM nodes at once.
- Optimization: None. No
React.memo, no specialized selectors. - Result:
- FPS: Drops to 0-5 FPS while scrolling.
- Interaction: Clicking a checkbox freezes the browser for 0.5s - 2.0s.
- Memory: High memory usage (holding thousands of DOM nodes).
- State: Normalized using
createEntityAdapter(Hash Map style). - Rendering: Uses Virtualization (Windowing) to render only the 20 visible rows.
- Optimization:
- O(1) Updates: Updating a task does not touch its neighbors.
- Memoization:
React.memoprevents re-renders if props haven't changed. - Reselect: Memoized selectors prevent expensive derived data calculations.
- Result:
- FPS: Solid 60 FPS.
- Interaction: Instantaneous (<2ms) updates.
- Memory: Low memory usage.
Problem: In a standard array [{ id: 1 }, { id: 2 }], finding or updating an item is O(n). Worse, in Redux, updating one item in an array changes the reference of the array, causing every component consuming that array to re-render.
Solution: We treat the Redux state like a database.
// Optimized State Shape
{
ids: ['task-1', 'task-2', ...],
entities: {
'task-1': { id: 'task-1', title: 'Pikachu', ... },
'task-2': { id: 'task-2', title: 'Charmander', ... }
}
}Why it wins:
- Lookups are O(1):
state.entities['task-500']is instant. - Stable References: Updating "Task 1" creates a new object for Task 1, but the object for "Task 2" remains the exact same memory reference.
Problem: The browser cannot handle 5,000 <div> elements. It destroys the layout engine and consumes massive RAM.
Solution: We use react-window List.
- We only render what fits on the screen (20 items).
- As you scroll, the library recycles the DOM nodes, just swapping the text content.
- Math: 5,000 items vs 20 items = 99.6% reduction in DOM weight.
In the Laggy version, clicking a checkbox updates the parent Array. This forces React to re-render the Entire List.
In the Optimized version:
- The Parent (
TaskList) only listens toids(which don't change on updates). - The Child (
TaskRow) is wrapped inReact.memo. - The Child selects its own data:
const task = useSelector(state => selectById(state, id)). - When Task 1 updates, Task 2's selector returns the same object.
React.memosees no changes and skips the render.
The app includes a built-in HUD (Heads Up Display), but here is how to verify deeper.
- Action: Click a checkbox.
- Observation:
- Optimized: Only the single row clicked changes background color.
- Laggy: The entire list flashes a new color.
- Note: Check the console logs to see the render counts.
- Open DevTools (
F12) -> Profiler tab. - Click Record.
- Click a checkbox in the app.
- Stop Recording.
- Analyze:
- Laggy: You will see a "Commit" taking 500ms+. You will see
LaggyRowrendered 5,000 times. - Optimized: You will see a "Commit" taking < 5ms. You will see
TaskRowrendered 1 time.
- Laggy: You will see a "Commit" taking 500ms+. You will see