The Problem: Unnecessary Re-renders
React's core strength is its reactivity—when a component's state or props change, it re-renders to reflect the new UI. However, this can be a weakness. By default, when a parent component re-renders, all of its children re-render too, even if their props haven't changed. For complex applications, this can lead to serious performance issues.
Our goal is to ensure components only re-render when they absolutely have to.
Memoization: "Remembering" Components and Values
Memoization is a fancy word for caching—storing the result of an expensive calculation and returning the cached result when the same inputs are used again. React provides three key hooks for this.
1. React.memo
React.memo is a higher-order component (HOC) that wraps a component and prevents it from re-rendering if its props have not changed.
Without React.memo: If the Counter component below re-renders, the Greeting component will also re-render, even though its name prop hasn't changed.
JavaScript
// Greeting.js
function Greeting({ name }) {
  console.log("Greeting was rendered");
  return <h1>Hello, {name}!</h1>;
}
// Counter.js
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
      <Greeting name="Alice" />
    </div>
  );
}
With React.memo: By wrapping Greeting in React.memo, we tell React to skip re-rendering it unless its props change.
JavaScript
import { memo } from 'react';
const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered");
  return <h1>Hello, {name}!</h1>;
});
export default Greeting;
Now, clicking the "Increment" button will not cause the "Greeting was rendered" message to appear in the console.
2. useMemo
This hook memoizes the return value of a function. It's perfect for expensive calculations that you don't want to re-run on every render.
JavaScript
import { useMemo, useState } from 'react';
function DataProcessor({ data }) {
  const [filter, setFilter] = useState('');
  // This complex filtering logic will ONLY re-run if `data` changes.
  // It will NOT re-run if the component re-renders for another reason (e.g., `filter` state changes).
  const veryComplexFilteredData = useMemo(() => {
    console.log("Filtering data...");
    return data.filter(item => item.includes('a')); // Imagine this is very slow
  }, [data]); // The dependency array
  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      {/* ... use veryComplexFilteredData ... */}
    </div>
  );
}
3. useCallback
This hook memoizes the function definition itself. This is crucial when passing callback functions to memoized child components. If you pass a new function instance on every render, React.memo will see it as a "changed" prop.
JavaScript
import { useState, useCallback, memo } from 'react';
// A memoized child component that takes a function as a prop
const MemoizedButton = memo(({ onClick }) => {
  console.log("Button rendered");
  return <button onClick={onClick}>Click me</button>;
});
function Parent() {
  const [count, setCount] = useState(0);
  // `useCallback` ensures that `handleButtonClick` is the SAME function
  // across re-renders, unless `count` changes.
  const handleButtonClick = useCallback(() => {
    console.log(`Button was clicked! Count is ${count}`);
  }, [count]); // Dependency array
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MemoizedButton onClick={handleButtonClick} />
    </div>
  );
}
Virtualization: Rendering Only What You See
What if you need to render a list with thousands of items? Rendering all of them at once would crash the browser. This is where virtualization (or "windowing") comes in.
The idea is simple: only render the small subset of items that are currently visible in the user's viewport. As the user scrolls, we swap out the items that are no longer visible with new ones. This keeps the number of DOM nodes low and the UI snappy.
While you can implement this yourself, libraries like react-window and react-virtualized make it easy.
JavaScript
import { FixedSizeList as List } from 'react-window';
// Imagine `data` is an array of 10,000 items
const MyHugeList = ({ data }) => {
  // The component for rendering a single row
  const Row = ({ index, style }) => (
    <div style={style}>
      Row {data[index].name}
    </div>
  );
  return (
    <List
      height={500} // Height of the list container
      itemCount={10000} // Total number of items
      itemSize={35} // Height of each row
      width={300} // Width of the list container
    >
      {Row}
    </List>
  );
};
This code will render a list that feels like it has 10,000 items, but it will only ever have a handful of them in the DOM at any given time.