Cuando React se siente lento, los re-renders innecesarios suelen ser los culpables. Los componentes se re-ejecutan más de lo necesario, y el coste acumulado se manifiesta como jank.
Una vez tuve una página con una lista de varios cientos de elementos. El scroll se sentía pesado. Abrí React DevTools y descubrí que cada componente se re-renderizaba en cada interacción — incluyendo los que no tenían nada que ver con el cambio.
Este artículo explica cómo funcionan los re-renders, y luego te muestra exactamente cómo usar memo, useMemo y useCallback para detener los innecesarios. No se trata de "esparcir memo por todos lados" — sino de entender por qué cada herramienta es necesaria en cada escenario.
¿Por Qué Ocurren los Re-renders?
React re-renderiza un componente en tres situaciones:
- Su propio estado cambió (se llamó a
setState) - Su padre se re-renderizó
- Un contexto que consume cambió
El segundo punto es el más sorprendente: cuando un padre se re-renderiza, todos sus hijos se re-renderizan por defecto — sin importar si sus props cambiaron.
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* ChildA and ChildB re-render every time count changes */}
<ChildA />
<ChildB />
</div>
);
}
ChildA y ChildB no reciben count como prop, sin embargo se re-renderizan cada vez que el estado de Parent cambia. A escala, esto se acumula en problemas reales de rendimiento.
Los Re-renders No Son Inherentemente Malos
Aclaración importante: los re-renders en sí no son malignos. React está diseñado para hacerlos rápidos, y las actualizaciones reales del DOM se minimizan al diff. Decenas de componentes simples re-renderizándose es imperceptible para los usuarios.
La optimización se vuelve necesaria cuando:
- Los componentes contienen cálculos costosos
- Las listas tienen cientos de elementos
- Necesitas animaciones fluidas a 60fps
Mide primero, optimiza después. Esto no es negociable.
Visualiza los Re-renders con React DevTools
Antes de optimizar, identifica qué se está re-renderizando innecesariamente.
Usa la pestaña Profiler en React DevTools (extensión del navegador):
- Instala React DevTools
- Abre la pestaña Profiler
- Haz clic en el ícono del engranaje → activa "Highlight updates when components render"
- Interactúa con tu app — los componentes re-renderizados parpadearán
Guía de colores:
- Azul — renders poco frecuentes (bien)
- Verde — moderado
- Amarillo — frecuente
- Rojo — muy frecuente (investiga)
React.memo: Omitir Re-renders Cuando las Props No Cambiaron
React.memo envuelve un componente y omite el re-renderizado cuando las props no han cambiado (comparadas con Object.is).
Sin Optimización
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
{/* NameDisplay re-renders every time count changes */}
<NameDisplay name={name} />
</div>
);
}
function NameDisplay({ name }: { name: string }) {
console.log("NameDisplay rendered"); // Called every time
return <p>Hello, {name}</p>;
}
Con React.memo
// Only re-renders when name actually changes
const NameDisplay = memo(function NameDisplay({ name }: { name: string }) {
console.log("NameDisplay rendered"); // Only called when name changes
return <p>Hello, {name}</p>;
});
Cuando memo No Funciona
function Parent() {
const [count, setCount] = useState(0);
// ❌ New object reference on every render
const user = { name: "Alice", age: 20 };
// ❌ New function reference on every render
const handleClick = () => console.log("clicked");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* user and handleClick have new references every render — memo can't help */}
<UserCard user={user} onClick={handleClick} />
</div>
);
}
const UserCard = memo(function UserCard({
user,
onClick,
}: {
user: { name: string; age: number };
onClick: () => void;
}) {
console.log("UserCard rendered"); // Still called every time
return (
<div onClick={onClick}>
{user.name} ({user.age})
</div>
);
});
Los objetos y funciones crean nuevas referencias en cada render. memo ve "las props cambiaron" y re-renderiza de todas formas. Aquí es donde entran useMemo y useCallback.
useMemo: Estabilizar Referencias de Objetos y Cachear Cálculos Costosos
useMemo memoriza un valor calculado, devolviendo la misma referencia a menos que las dependencias cambien.
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
const [age, setAge] = useState(20);
// ✅ New object only when name or age changes
const user = useMemo(() => ({ name, age }), [name, age]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
{/* When only count changes, user reference stays stable — UserCard skips re-render */}
<UserCard user={user} />
</div>
);
}
const UserCard = memo(function UserCard({
user,
}: {
user: { name: string; age: number };
}) {
console.log("UserCard rendered");
return <div>{user.name} ({user.age})</div>;
});
Cachear Cálculos Costosos
function SearchResults({ items, query }: { items: Item[]; query: string }) {
// ❌ Filter runs on every render
// const filtered = items.filter(item => item.name.includes(query));
// ✅ Filter only runs when items or query changes
const filtered = useMemo(
() => items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
),
[items, query]
);
return (
<ul>
{filtered.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Para más de 1.000 elementos con filtrado o clasificación compleja, useMemo vale la pena. Para 10 elementos simples, el overhead de useMemo probablemente excede el ahorro.
useCallback: Estabilizar Referencias de Funciones
useCallback memoriza una función, devolviendo la misma referencia a menos que las dependencias cambien.
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Alice");
// ✅ New function only when name changes
const handleUserClick = useCallback(() => {
console.log(`${name} was clicked`);
}, [name]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
{/* When only count changes, handleUserClick reference stays stable */}
<UserCard onClick={handleUserClick} />
</div>
);
}
const UserCard = memo(function UserCard({ onClick }: { onClick: () => void }) {
console.log("UserCard rendered");
return <button onClick={onClick}>Click me</button>;
});
useMemo vs useCallback
useCallback(fn, deps) es equivalente a useMemo(() => fn, deps). Piensa en useCallback como un atajo para memorizar funciones:
// These two are identical
const fn1 = useCallback(() => doSomething(a, b), [a, b]);
const fn2 = useMemo(() => () => doSomething(a, b), [a, b]);
Ejemplo Real: Optimizando una Lista Pesada
Veamos una optimización completa de una lista grande de tareas.
Antes
function TodoList() {
const [todos, setTodos] = useState(initialTodos); // 500 items
const [filter, setFilter] = useState("all");
// Filtering runs on every render
const filtered = todos.filter(todo => {
if (filter === "done") return todo.done;
if (filter === "pending") return !todo.done;
return true;
});
// New function references on every render
const handleToggle = (id: string) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
};
const handleDelete = (id: string) => {
setTodos(prev => prev.filter(t => t.id !== id));
};
return (
<div>
<FilterButtons filter={filter} onChange={setFilter} />
<ul>
{filtered.map(todo => (
// Every TodoItem re-renders when filter changes
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
);
}
Después
function TodoList() {
const [todos, setTodos] = useState(initialTodos); // 500 items
const [filter, setFilter] = useState("all");
// ✅ Filter only recalculates when todos or filter changes
const filtered = useMemo(
() => todos.filter(todo => {
if (filter === "done") return todo.done;
if (filter === "pending") return !todo.done;
return true;
}),
[todos, filter]
);
// ✅ Stable references — functional updates mean no need for todos in deps
const handleToggle = useCallback((id: string) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
}, []);
const handleDelete = useCallback((id: string) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, []);
return (
<div>
<FilterButtons filter={filter} onChange={setFilter} />
<ul>
{filtered.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} // Stable reference
onDelete={handleDelete} // Stable reference
/>
))}
</ul>
</div>
);
}
// ✅ memo wrap — skips re-render when todo, onToggle, onDelete haven't changed
const TodoItem = memo(function TodoItem({
todo,
onToggle,
onDelete,
}: {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<li>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => onToggle(todo.id)}>Toggle</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
Qué cambió:
useMemopara el filtrado — el filtro de 500 elementos ya no se ejecuta en renders no relacionadosuseCallbackpara los handlers — las actualizaciones funcionales (prev =>) permiten omitirtodosde las depsmemoen TodoItem — los elementos individuales omiten re-renders cuando nada cambió para ellos
Prevenir Re-renders Causados por Context
Cuando un valor de contexto cambia, cada componente que consume ese contexto se re-renderiza. Sobrecargar un solo contexto causa re-renders en cascada.
// ❌ Everything in one context
const AppContext = createContext({
user: null,
theme: "dark",
notifications: [],
});
// A notification update causes components only using user to re-render
// ✅ Split by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext("dark");
const NotificationContext = createContext([]);
// Components using UserContext only re-render when user changes
Agrupa los valores del contexto según la frecuencia con la que cambian juntos.
Conclusión
La optimización de re-renders sigue este orden: medir → identificar → corregir.
| Herramienta | Propósito | Cuándo Usar |
|---|---|---|
React.memo | Omitir re-renders cuando las props no cambian | Componentes pesados con re-renders innecesarios confirmados |
useMemo | Cachear valores y estabilizar referencias de objetos | Cálculos costosos, objetos pasados a componentes memorizados |
useCallback | Estabilizar referencias de funciones | Funciones pasadas a componentes memorizados, funciones en deps de useEffect |
"Esparcir memo por todos lados" frecuentemente empeora las cosas. Abre el Profiler de React DevTools, confirma qué re-renders son innecesarios, rastrea la causa y aplica la corrección adecuada.
Prioridad de optimización:
- Mide y confirma que el problema existe
- Considera soluciones estructurales (colocación de componentes) primero
- Luego recurre a
memo→useCallback→useMemo