32blogby Studio Mitsu

Optimización de Re-renders en React: Guía Práctica

Detén los re-renders innecesarios con memo, useMemo y useCallback. Aprende cuándo usar cada uno, cómo medir y errores comunes.

by omitsu12 min read
ReactperformancememouseMemouseCallbackre-render
Contenido

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:

  1. Su propio estado cambió (se llamó a setState)
  2. Su padre se re-renderizó
  3. 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.

tsx
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):

  1. Instala React DevTools
  2. Abre la pestaña Profiler
  3. Haz clic en el ícono del engranaje → activa "Highlight updates when components render"
  4. 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

tsx
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

tsx
// 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

tsx
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.

tsx
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

tsx
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.

tsx
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:

tsx
// 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

tsx
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

tsx
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ó:

  1. useMemo para el filtrado — el filtro de 500 elementos ya no se ejecuta en renders no relacionados
  2. useCallback para los handlers — las actualizaciones funcionales (prev =>) permiten omitir todos de las deps
  3. memo en 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.

tsx
// ❌ 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
tsx
// ✅ 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.

HerramientaPropósitoCuándo Usar
React.memoOmitir re-renders cuando las props no cambianComponentes pesados con re-renders innecesarios confirmados
useMemoCachear valores y estabilizar referencias de objetosCálculos costosos, objetos pasados a componentes memorizados
useCallbackEstabilizar referencias de funcionesFunciones 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:

  1. Mide y confirma que el problema existe
  2. Considera soluciones estructurales (colocación de componentes) primero
  3. Luego recurre a memouseCallbackuseMemo

Artículos relacionados: