Para detener los re-renders innecesarios en React, usa React.memo para omitir re-renders cuando las props no cambian, useMemo para estabilizar referencias de objetos y cachear cálculos costosos, y useCallback para estabilizar referencias de funciones. Siempre mide con React DevTools Profiler antes de optimizar.
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.
Si tienes una página con una lista de cientos de elementos y el scroll se siente pesado, abre el React DevTools Profiler. Si ves componentes parpadeando en cada interacción — incluyendo los que no tienen nada que ver con el cambio — eso es señal de que necesitas optimizar.
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 y ChildB se re-renderizan cada vez que count cambia */}
<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 se re-renderiza cada vez que count cambia */}
<NameDisplay name={name} />
</div>
);
}
function NameDisplay({ name }: { name: string }) {
console.log("NameDisplay rendered"); // Se llama cada vez
return <p>Hola, {name}</p>;
}
Con React.memo
// Solo se re-renderiza cuando name realmente cambia
const NameDisplay = memo(function NameDisplay({ name }: { name: string }) {
console.log("NameDisplay rendered"); // Solo se llama cuando name cambia
return <p>Hola, {name}</p>;
});
Cuando memo No Funciona
function Parent() {
const [count, setCount] = useState(0);
// ❌ Nueva referencia de objeto en cada render
const user = { name: "Alice", age: 20 };
// ❌ Nueva referencia de función en cada render
const handleClick = () => console.log("clicked");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* user y handleClick tienen nuevas referencias en cada render — memo no puede ayudar */}
<UserCard user={user} onClick={handleClick} />
</div>
);
}
const UserCard = memo(function UserCard({
user,
onClick,
}: {
user: { name: string; age: number };
onClick: () => void;
}) {
console.log("UserCard rendered"); // Se sigue llamando cada vez
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);
// ✅ Nuevo objeto solo cuando name o age cambian
const user = useMemo(() => ({ name, age }), [name, age]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
{/* Cuando solo count cambia, la referencia de user se mantiene estable — UserCard omite el 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 }) {
// ❌ El filtro se ejecuta en cada render
// const filtered = items.filter(item => item.name.includes(query));
// ✅ El filtro solo se ejecuta cuando items o query cambian
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");
// ✅ Nueva función solo cuando name cambia
const handleUserClick = useCallback(() => {
console.log(`${name} fue clickeado`);
}, [name]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
{/* Cuando solo count cambia, la referencia de handleUserClick se mantiene estable */}
<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:
// Estas dos líneas son idénticas
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 elementos
const [filter, setFilter] = useState("all");
// El filtrado se ejecuta en cada render
const filtered = todos.filter(todo => {
if (filter === "done") return todo.done;
if (filter === "pending") return !todo.done;
return true;
});
// Nuevas referencias de función en cada 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 => (
// Cada TodoItem se re-renderiza cuando filter cambia
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
);
}
Después
function TodoList() {
const [todos, setTodos] = useState(initialTodos); // 500 elementos
const [filter, setFilter] = useState("all");
// ✅ El filtro solo se recalcula cuando todos o filter cambian
const filtered = useMemo(
() => todos.filter(todo => {
if (filter === "done") return todo.done;
if (filter === "pending") return !todo.done;
return true;
}),
[todos, filter]
);
// ✅ Referencias estables — las actualizaciones funcionales no necesitan todos en 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} // Referencia estable
onDelete={handleDelete} // Referencia estable
/>
))}
</ul>
</div>
);
}
// ✅ Envuelto con memo — omite el re-render cuando todo, onToggle, onDelete no han cambiado
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.
// ❌ Todo en un solo contexto
const AppContext = createContext({
user: null,
theme: "dark",
notifications: [],
});
// Una actualización de notifications causa re-render en componentes que solo usan user
// ✅ Dividir por frecuencia de actualización
const UserContext = createContext(null);
const ThemeContext = createContext("dark");
const NotificationContext = createContext([]);
// Los componentes que usan UserContext solo se re-renderizan cuando user cambia
Agrupa los valores del contexto según la frecuencia con la que cambian juntos.
¿Qué Pasa con el React Compiler?
El React Compiler (actualmente en beta) analiza tu código en tiempo de compilación e inserta automáticamente memoización donde sea necesario — haciendo lo que memo, useMemo y useCallback hacen manualmente.
¿Eso significa que puedes olvidarte de la optimización de re-renders? No del todo:
- El Compiler es opt-in y todavía está en beta. La mayoría de apps en producción aún no lo usan
- Entender por qué ocurren los re-renders te ayuda a escribir mejor arquitectura de componentes (dividir contextos, colocar estado) — cosas que el Compiler no puede arreglar por ti
- Casos complejos como funciones de comparación personalizadas o decisiones arquitectónicas siguen necesitando criterio humano
Piensa en el Compiler como una red de seguridad, no como un reemplazo del entendimiento. Los patrones de este artículo siguen siendo la base, y el Compiler construye sobre ellos.
FAQ
¿Debería envolver cada componente con React.memo?
No. memo añade un coste de comparación de props en cada render. Para componentes simples y ligeros, este overhead puede superar el ahorro. Usa memo donde hayas confirmado re-renders innecesarios en DevTools — típicamente componentes pesados o elementos de lista que se re-renderizan por cambios en el estado del padre.
¿Cuál es la diferencia entre useMemo y useCallback?
useCallback(fn, deps) es un atajo para useMemo(() => fn, deps). useMemo cachea un valor (incluyendo objetos); useCallback cachea una referencia de función. Usa useCallback cuando pasas funciones a componentes memorizados; usa useMemo para cálculos costosos o estabilizar referencias de objetos.
¿El React Compiler reemplaza memo, useMemo y useCallback?
El React Compiler automatiza la memoización en tiempo de compilación, así que no necesitarás escribir estos hooks manualmente en la mayoría de casos. Sin embargo, todavía está en beta y es opt-in. Entender los fundamentos te ayuda a escribir código que el Compiler pueda optimizar eficazmente.
¿Por qué mi componente memorizado se sigue re-renderizando?
La causa más común: estás pasando una nueva referencia de objeto o función como prop. memo usa Object.is para la comparación, así que { name: "Alice" } !== { name: "Alice" } porque son referencias diferentes. Estabiliza las props con useMemo (objetos) y useCallback (funciones).
¿Funciona memo con children props?
No muy bien. Los children JSX como <Wrapper><Child /></Wrapper> crean nuevas referencias de elementos React en cada render, anulando memo. Considera usar el patrón children-as-props o reestructurar tu árbol de componentes.
¿Puedo usar memo con React Server Components?
Los Server Components se ejecutan en el servidor y no se re-renderizan en el cliente, así que memo es innecesario. Usa memo solo en Client Components ('use client') donde hayas identificado problemas de re-render.
¿Cómo mido el rendimiento de los re-renders?
Usa el Profiler de React DevTools: activa "Highlight updates when components render" para ver los re-renders visualmente, luego usa el flamegraph para identificar qué componentes toman más tiempo. La pestaña Performance de Chrome DevTools también puede mostrar Long Tasks causados por re-renders excesivos.
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
Artículos relacionados: