react-memoizacion-rendimiento

Memoización en React

  • 5 min

React es rápido. Por defecto, está diseñado para que no tengáis que preocuparos por el rendimiento en el 90% de los casos.

Sin embargo, cuando las aplicaciones crecen, empezamos a notar cierto “lag” al escribir en un input o al abrir un menú. Generalmente, esto se debe a uno de los problemas más habituales de React, los renderizados innecesarios.

Un renderizado innecesario es que un componente que se dibuja (a él y a sus hijos), sin realmente ser necesario porque no habia cambiado nada.

Uno de los mecanismos para mejorar esto es la Memoización, una técnica que permite “recordar” resultados anteriores para no tener que volver a calcularlos.

El problema del renderizado en cascada

Para entender la solución, primero hay que entender el problema. En React, cuando un componente padre se renderiza (porque cambió su estado), todos sus hijos se renderizan también recursivamente, hayan cambiado sus props o no.

Imaginad esto:

Tenéis un componente Padre con un estado descripción (por ejemplo).

Tenéis un componente Hijo que es una lista de 5.000 elementos estática.

Cada vez que incrementáis la descripción del padre, React vuelve a pintar la lista de 5.000 elementos, aunque esta no cambie en absoluto 😱.

Esto es una locura ineficiente. Aquí es donde entran nuestras tres herramientas de optimización.


Protegiendo componentes React.memo

React.memo es un Higher-Order Component (HOC) que envuelve a vuestro componente. Su función es verificar las props:

Si las props que recibe son idénticas a las de la vez anterior, no re-renderiza, reutilizo el resultado anterior

La sintaxis es la siguiente,

import { memo, useState } from 'react';

// Componente Hijo (Pesado)
// Al envolverlo en memo, lo protegemos.
const HijoPesado = memo(function Hijo({ texto }) {
  console.log("Renderizando Hijo..."); // Solo saldrá si 'texto' cambia
  return <p>{texto}</p>;
});

export default function Padre() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      
      {/* Aunque Padre cambie, 'texto' sigue siendo "Hola", 
          así que HijoPesado NO se renderizará de nuevo. */}
      <HijoPesado texto="Hola" />
    </div>
  );
}
Copied!
Que un hijo pesado le pongamos “memo”, es una fantasía de sintaxis

React.memo hace una comparación superficial (Object.is). Si pasáis objetos o funciones como props, necesitaréis las herramientas que veremos a continuación, o la memoización fallará.

Estabilizando funciones con useCallback

Este es el Hook más difícil de entender, porque tiene que ver con la Identidad Referencial de JavaScript.

En JS, function() {} !== function() {}.

Cada vez que un componente se renderiza, todas las funciones que hay dentro se crean de nuevo. Son funciones nuevas en direcciones de memoria nuevas.

Vemos el problema. Volvamos a React.memo. Dijimos que solo evita el renderizado si las props no cambian.

function Padre() {
  const [count, setCount] = useState(0);

  // Esta función SE CREA DE NUEVO en cada click
  const handleClick = () => console.log('Click');

  return (
    // ¡React.memo NO funcionará!
    <HijoMemoizado onClick={handleClick} />
  );
}
Copied!

Para el Hijo, la prop ‘onClick’ ha cambiado (es una función nueva), así que se renderiza igual.

Para evitarlo useCallback congela la función. Devuelve exactamente la misma instancia de la función entre renderizados, a menos que cambien sus dependencias.

import { useCallback } from 'react';

function Padre() {
  const [count, setCount] = useState(0);

  // ✅ Ahora 'handleClick' es estable. Es siempre la misma referencia en memoria.
  const handleClick = useCallback(() => {
    console.log('Click');
  }, []); // Dependencias vacías = nunca cambia

  return (
    // 😄 Ahora sí: HijoMemoizado recibe la misma prop y NO se renderiza.
    <HijoMemoizado onClick={handleClick} />
  );
}
Copied!

Solo necesitáis useCallback si vais a pasar esa función como prop a un componente envuelto en React.memo o si la función es dependencia de un useEffect.

En el resto de casos, no aporta nada.

El coste de la memoización (a.k.a. no lo uses para todo)

Si estas herramientas son tan buenas, ¿por qué no envolver todo en memo y useCallback? Porque la memoización no es gratis.

  1. Consume Memoria: React tiene que guardar en memoria los inputs anteriores y el resultado anterior.
  2. Coste de CPU: En cada render, React tiene que comparar las props nuevas con las viejas.

Si el componente es simple (ej: un <Button>Texto</Button>), cuesta más trabajo comparar las props que renderizar el botón de nuevo.

  • Componentes visualmente grandes (Listas, Tablas, Gráficos)
  • Componentes que se renderizan muy a menudo (al hacer scroll, al escribir en un input)
  • Cuando notáis lentitud real
  • Botones simples, Iconos, Textos
  • Componentes que siempre cambian (si las props cambian siempre, memo trabaja para nada)
  • “Por si acaso”, o porque te aburres… (premature optimization)