Ir al contenido
wannacode

Velocidad: cuándo se vuelve a renderizar un componente

En resumen: En React un componente se vuelve a renderizar cuando cambia su estado o su padre; primero mides, y después memo, useMemo y useCallback detienen los renders innecesarios.

Problema

Cada letra renderiza toda la lista

Arriba hay un campo de búsqueda y un botón «Buscar». La lista se filtra solo después de «Buscar». ProductList escribe en la consola cada vez que se renderiza. Escribe «leche» en el campo y mira la consola.

React

Cinco letras, cinco líneas nuevas «renderizo ProductList». La lista es la misma: todavía no has pulsado «Buscar». Aquí hay ocho productos y no lo notas. En un catálogo real hay 2000, cada uno con imagen y precio, y el campo empieza a ir por detrás de tus dedos.

La idea

Cuándo se vuelve a renderizar un componente

Un componente se vuelve a renderizar cuando cambian los datos que muestra. Pero qué son «sus datos» lo deciden de forma distinta las dos bibliotecas. Las dos dan el resultado correcto; la diferencia es cuánto trabajo hacen.

React vuelve a renderizar el componente cuyo estado cambió y todos sus hijos, hacia abajo por todo el árbol. Por defecto no comprueba si las props de un hijo son las mismas. Aquí draft está en App, así que cada letra renderiza App, ProductList y cada tarjeta.

Esto suele ser rápido: React compara el resultado con la página antigua y cambia en el DOM solo la diferencia. Se vuelve caro cuando el subárbol es grande o el cálculo que hay en él es pesado.

Primero mide, después arregla. Un contador en la consola, como aquí, es la medición más sencilla. Las React DevTools tienen una pestaña Profiler: grabas, escribes en el campo y ves qué componente se renderizó cuántas veces y en cuántos milisegundos. Arreglas solo un render que sea a la vez innecesario y lento.

Míralo

Mira quién se ilumina

const ProductList = memo(function ProductList({ products, onAdd }) {
console.log(`renderizo ProductList: ${++renders}`);
return products.map((p) => <ProductCard key={p.id} product={p} onAdd={onAdd} />);
});
function App() {
const [draft, setDraft] = useState('');
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
const visible = useMemo(() => filterByName(PRODUCTS, query), [query]);
const add = useCallback(() => setCount((c) => c + 1), []);
return (
<>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button onClick={() => setQuery(draft)}>Buscar</button>
<ProductList products={visible} onAdd={add} />
</>
);
}
Paso a paso, en palabras
  1. App guarda el texto del campo (draft) y la búsqueda (query). ProductList dibuja las tarjetas. La consola cuenta cuántas veces se renderiza la lista.
  2. Cada tecla cambia draft. React vuelve a renderizar App, y con ella cada hijo: ProductList y cada tarjeta, aunque la lista es la misma.
  3. memo(ProductList) compara las props nuevas con las antiguas. useMemo conserva el mismo array mientras query sea la misma, y useCallback conserva la misma add.
  4. Ahora una tecla renderiza solo App y el campo. Las props de ProductList son las mismas, así que se omite, y el contador se queda.
  5. «Buscar» cambia query. El array es nuevo, así que ProductList se renderiza. Este render es necesario: la lista muestra ahora dos productos.
  6. Primero mide: un contador en la consola o el Profiler de las DevTools. Arregla solo los renders que son a la vez innecesarios y lentos.
React solo puede omitir un hijo si le dices que compare las props: eso es memo. Y la comparación tiene sentido solo si las props realmente son las mismas: el array y la función deben conservarse entre renders.

Pruébalo

Solo «Buscar» renderiza la lista

Haz que escribir en el campo y «Añadir» no vuelvan a renderizar ProductList. Solo «Buscar» debe renderizarla, porque solo entonces cambia la lista. La comprobación escribe «leche», añade dos productos, pulsa «Buscar» y cuenta las líneas «renderizo ProductList» de la consola.

React

Pruébalo

Encuentra el error

ProductList está en memo, y la lista se sigue renderizando con cada letra. ¿Qué línea tiene la culpa?

Elige la línea con el error.

Pruébalo

Completa

¿Cómo se mantiene igual la lista cara entre renders?

const visible = (
  () => PRODUCTS.filter((p) => p.name.includes(query)),
  [query],
);

La idea

Una lista larga: renderizar menos

El elemento más rápido es el que no se dibuja. Una persona ve 10–20 productos en la pantalla, no 2000. Por eso una lista larga dibuja solo una parte:

  • «Mostrar más»: los primeros 20, y con un clic 20 más. Solo se recuerda cuántos se muestran.
  • Páginas: «1, 2, 3 …», a menudo con el número en la dirección (/?page=2), para poder compartirla.
  • Una lista virtual (windowing): dibuja solo las filas visibles y las cambia al desplazarse. Es difícil de escribir, así que usas una biblioteca: TanStack Virtual o react-window.

Las claves de la lección sobre listas ayudan también aquí: con key={p.id} los 20 productos añadidos son elementos nuevos, y los antiguos se quedan sin rehacerse.

La idea

Carga diferida

No todas las partes de la página hacen falta de golpe. Las reseñas del producto, el mapa de las tiendas, el editor del perfil: pueden cargarse solo cuando la persona los abre. Entonces la primera carga descarga menos código.

import { lazy, Suspense } from 'react';
const Reviews = lazy(() => import('./Reviews.jsx'));
<Suspense fallback={<p>Cargando las reseñas…</p>}>
<Reviews />
</Suspense>

import('./Reviews.jsx') es una llamada a una función que carga el archivo y devuelve una promesa. lazy la convierte en un componente. Mientras el archivo viene en camino, Suspense muestra el fallback.

React

Abre la consola: la línea de reviews.js aparece solo después del clic. Para ser sinceros, en el sandbox todos los archivos ya están en la página, así que aquí import() solo ejecuta el archivo más tarde. En un proyecto real Vite pone cada uno de esos archivos aparte y el navegador lo descarga del servidor en el primer clic. Lo mismo vale para las páginas: con React Router cualquier página puede ser lazy.

Más precisamente, para curiosos

React Compiler. Si el proyecto usa React Compiler, él añade por su cuenta este tipo de memorización al construir el código, y rara vez hacen falta memo, useMemo y useCallback a mano. La documentación de React lo recomienda para código nuevo y mantiene useMemo y useCallback como medio de control preciso.

El estado hacia abajo. Otra solución sin memo: el campo con su draft pasa a ser un componente aparte SearchBox que avisa hacia arriba solo con «Buscar» (onSearch(draft)). Entonces las letras cambian el estado en SearchBox, y App no se renderiza en absoluto. Es la regla «tan arriba como haga falta y no más» de la lección sobre elevar el estado.

StrictMode. En desarrollo <StrictMode> renderiza cada componente dos veces, para detectar componentes impuros. El contador entonces crece de dos en dos. La versión construida no lo hace.

useMemo es una pista. React puede descartar el resultado recordado y calcularlo de nuevo. El código debe ser correcto también sin él.

El modo de desarrollo es más lento. Las advertencias y las comprobaciones cuestan tiempo. Mide la velocidad de la versión construida (vite build y vite preview), no solo en desarrollo.

Ejercicio

Mostrar más

El catálogo tiene 10 productos y los dibuja todos. Muestra los primeros 4, y debajo «Mostrando 4 de 10» y un botón «Mostrar más» que añade 4 más. Cuando todos están mostrados, el botón desaparece. La comprobación cuenta los productos después de cada clic y mira el texto y el botón.

React

Test

Ponte a prueba

1. La página de producto va lenta. ¿Qué es lo primero que haces?
2. ProductList está en memo. App pasa products={PRODUCTS.filter(…)} y onAdd={() => add()}. Escribes en el campo de App. ¿Se renderiza ProductList?
3. El catálogo tiene 5000 productos y la página se abre despacio. ¿Qué ayuda más?
4. ¿Qué aporta la carga diferida de un componente (lazy / defineAsyncComponent) en un proyecto real?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·