El Módulo 7 terminó con un diagnóstico incómodo: CicloUrbano sabe dónde vive cada dato y por qué, pero no va rápida. Hay componentes que se repintan sin motivo, cálculos que se rehacen en cada render y un paquete que el navegador descarga entero antes de enseñar la primera bicicleta. La tentación natural al llegar aquí es abrir el editor y empezar a repartir memo y useMemo por el proyecto hasta que «se note mejor». Esa tentación es exactamente lo que esta lección viene a desactivar. Optimizar sin medir no es optimizar: es añadir complejidad a ciegas y esperar que salga bien. Antes de tocar una sola línea hay que saber qué significa «lento» para el usuario, por qué se vuelve a ejecutar un componente, qué es realmente caro y qué no, y —lo más importante— que la mayoría de los problemas de rendimiento de una aplicación React se resuelven mejor sin memorizar nada. Esta lección es el panorama y el método: las técnicas que casi siempre rinden más que la memorización, el papel del React Compiler de React 19 y el flujo de trabajo que ordena todo el módulo. Las herramientas de memorización llegan en las lecciones siguientes, y llegan mejor preparadas después de esto.

Contenido

  1. La regla que gobierna el módulo: medir primero
  2. El coste real de la optimización prematura
  3. Qué significa «lento» en una interfaz: las métricas que le importan al usuario
  4. Por qué se vuelve a ejecutar un componente
  5. El malentendido más caro del ecosistema
  6. Técnica 1: bajar el estado al componente que lo usa
  7. Técnica 2: pasar children para aislar un subárbol
  8. Técnica 3: derivar durante el render en vez de guardar y sincronizar
  9. Técnica 4: claves estables en listas
  10. Técnica 5: retrasar y limitar las entradas de texto
  11. Técnica 6: paginar y virtualizar listas largas
  12. Técnica 7: sacar trabajo del hilo principal
  13. El React Compiler de React 19: qué automatiza y qué no
  14. Presupuesto de rendimiento y flujo de trabajo
  15. Mapa del resto del módulo

  1. La regla que gobierna el módulo: medir primero

No optimices lo que no has medido. Si no puedes señalar un número que empeora, no tienes un problema de rendimiento: tienes una sospecha.

Esta regla suena a consejo de manual y es una decisión de ingeniería con consecuencias muy concretas. Los motivos por los que la intuición falla en rendimiento web son sistemáticos:

  • Tu máquina no es la del usuario. Desarrollas en un portátil potente con la aplicación en localhost y sin latencia de red. El usuario de CicloUrbano abre el catálogo en un móvil de gama media, con 4G irregular, mientras camina hacia la estación.
  • El modo de desarrollo miente. El servidor de Vite sirve módulos sin empaquetar, React registra avisos e información de depuración, y StrictMode ejecuta cada componente dos veces. Un render que en desarrollo tarda 18 ms puede tardar 4 ms en producción.
  • Lo que se siente lento y lo que es lento rara vez coinciden. Un render de 300 ms al pulsar un botón es una catástrofe percibida; el mismo trabajo repartido en un setTimeout de fondo no lo nota nadie.
  • Los cuellos de botella se esconden. El componente que sospechas es casi nunca el culpable. En CicloUrbano, el culpable de que escribir en el buscador se sienta pastoso no es el <input>: son las tarjetas que se repintan detrás.

La consecuencia práctica es que el orden de trabajo correcto empieza siempre por la lección 08-05 (el Profiler), aunque sea la última del módulo. Este módulo se estudia en orden porque hace falta conocer las herramientas para entender lo que el Profiler enseña, pero se aplica en el orden inverso: medir, localizar, corregir, volver a medir.

  1. El coste real de la optimización prematura

Cuando alguien dice que la optimización prematura es cara, suele quedarse en lo abstracto. Estos son los costes reales, uno a uno.

Coste En qué consiste Ejemplo en CicloUrbano
Legibilidad El código deja de decir lo que hace y pasa a decir cómo lo hace rápido const alReservar = useCallback((id) => …, [usuario, mostrarAviso, crearReserva]) frente a una función normal de tres líneas
Memoria Cada valor memorizado se guarda. Memorizar 2.000 tarjetas es guardar 2.000 comparaciones y 2.000 resultados Un memo en cada componente hoja de un catálogo largo
Trabajo añadido Comparar props también cuesta. Si el componente es barato, comparar sale más caro que volver a ejecutarlo memo sobre EtiquetaEstado, que solo pinta un <span>
Errores por dependencias Un array de dependencias incompleto congela un valor obsoleto; uno excesivo anula la memorización sin avisar Un useMemo que olvida orden y sigue mostrando el catálogo ordenado como estaba
Falsa sensación de trabajo hecho Se cierra el asunto sin haber tocado el problema real Memorizar todo el catálogo cuando lo que sobra son 900 KB de JavaScript inicial

El más grave, con diferencia, es el cuarto. Un memo innecesario ralentiza un poco; un useMemo con dependencias mal puestas produce datos incorrectos en pantalla, y ese fallo es intermitente, difícil de reproducir y suele descubrirlo un usuario.

Un error de rendimiento hace que la aplicación vaya lenta. Un error de memorización hace que la aplicación mienta.

  1. Qué significa «lento» en una interfaz: las métricas que le importan al usuario

«Lento» no es una sensación: es un conjunto de magnitudes medibles. Estas son las que la industria ha consolidado (las llamadas Web Vitals), con lo que significan y —dato clave para este módulo— cuánto puede hacer React por cada una.

Métrica Qué mide el usuario Objetivo razonable ¿Depende de React?
FCP (First Contentful Paint, primer pintado con contenido) Cuánto tarda en aparecer algo en la pantalla en blanco < 1,8 s Parcialmente: sobre todo del tamaño del paquete (08-04) y del servidor
LCP (Largest Contentful Paint, pintado del elemento principal) Cuánto tarda en verse el contenido protagonista: la lista de bicicletas < 2,5 s Parcialmente: paquete, datos y coste del primer render
TTI (Time To Interactive, tiempo hasta interactivo) Cuándo la página responde de verdad a un clic < 3,8 s : JavaScript descargado, analizado y ejecutado
INP (Interaction to Next Paint, retardo de la interacción) Cuánto tarda la pantalla en reaccionar a un clic o a una tecla < 200 ms Sí, mucho: es la métrica de este módulo
CLS (Cumulative Layout Shift, estabilidad visual) Cuánto «salta» la maquetación mientras carga < 0,1 : indicadores de carga y esqueletos mal diseñados (08-04)
TBT (Total Blocking Time, bloqueo del hilo principal) Cuánto tiempo el navegador no puede atender al usuario < 200 ms : renders largos, cálculos en el render

De esta tabla se extraen dos conclusiones que ordenan el módulo entero:

  1. INP y TBT son territorio de React. Cuando escribir en el BuscadorBicicletas tarda 400 ms en reflejarse, eso es INP, y se arregla con memorización, useDeferredValue o virtualización (08-02, 08-03).
  2. FCP y LCP son sobre todo territorio del empaquetador. Que el navegador tenga que descargar 1,2 MB de JavaScript antes de pintar el catálogo no lo arregla ningún memo: lo arregla la división de código (08-04).

Y una tercera, incómoda: hay causas de lentitud que no son de React en absoluto y ninguna técnica de este módulo va a tocar — imágenes de estaciones sin comprimir ni dimensionar, una consulta a json-server que devuelve las 2.000 bicicletas sin paginar, tipografías que bloquean el pintado, o una petición en cascada que espera a otra. Antes de memorizar nada, comprueba que el problema está donde crees.

  1. Por qué se vuelve a ejecutar un componente

En 01-05 quedó establecido el ciclo de React: render → diferencias (diff) → confirmación (commit). Ahora hace falta la parte que aquel modelo dejaba implícita: qué hace que React vuelva a llamar a la función de un componente. Solo hay cuatro causas, y no hay una quinta.

Causa Descripción Ejemplo
1. Su propio estado cambia Un setEstado (o un dispatch) con un valor distinto según Object.is BuscadorBicicletas al escribir una letra
2. Un ancestro se vuelve a renderizar React reejecuta el subárbol devuelto por el padre, cambien o no las props TarjetaBicicleta cuando PaginaCatalogo se repinta
3. Cambia un contexto que consume Todo consumidor de ese contexto se reejecuta, aunque use solo una parte del valor MenuUsuario cuando cambia el tema, si comparten proveedor
4. Cambia un valor suscrito de un almacén externo useSelector de Redux, useQuery de TanStack Query, useSyncExternalStore ResumenFlota cuando llega una revalidación

Fíjate en lo que no está en la lista: «que le hayan cambiado las props». Las props no disparan nada por sí solas. Un componente recibe props nuevas porque su padre se ha vuelto a ejecutar (causa 2), y ese es el orden causal correcto.

flowchart TD
    A["setEstado en PaginaCatalogo"] --> B["React reejecuta<br/>PaginaCatalogo"]
    B --> C["Reejecuta TODO su subárbol<br/>hayan cambiado las props o no"]
    C --> D["BuscadorBicicletas"]
    C --> E["SelectorTipo"]
    C --> F["ListaBicicletas"]
    F --> G["TarjetaBicicleta x N"]
    G --> H{"¿El JSX resultante<br/>difiere del anterior?"}
    H -->|No| I["React no toca el DOM<br/>coste: solo la funcion JS"]
    H -->|Si| J["React aplica los cambios<br/>minimos al DOM"]

  1. El malentendido más caro del ecosistema

Aquí está la frase que hay que interiorizar antes de escribir un solo memo:

Que un componente se vuelva a renderizar no significa que el navegador vuelva a pintar nada. Significa que React vuelve a llamar a una función de JavaScript y compara el resultado.

Un render es: ejecutar la función del componente, obtener un árbol de objetos ligeros (elementos de React), compararlo con el anterior y aplicar al DOM solo lo que ha cambiado. Si TarjetaBicicleta devuelve exactamente el mismo JSX que antes, el DOM no se toca. Esto cambia por completo la escala del problema:

Operación Coste orientativo ¿Preocupante?
Ejecutar la función de un componente sencillo 0,01 – 0,1 ms No
Comparar su árbol de elementos Proporcional al número de nodos, muy barato No
Ejecutarlo 2.000 veces (catálogo completo) 20 – 200 ms : se nota
Escribir en el DOM real (crear, mover o borrar nodos) 0,5 – 5 ms por nodo , es lo caro
Provocar un recálculo de maquetación (layout) 5 – 50 ms , muy caro
Ordenar o filtrar 2.000 objetos 1 – 15 ms por render Depende de con qué frecuencia ocurra
Formatear fechas con Intl 2.000 veces 50 – 300 ms : Intl es sorprendentemente caro

La lectura correcta: un render de más no es un error, es un dato. Cien renders de componentes triviales pueden ser irrelevantes; un solo render que ordena un array de 2.000 elementos y formatea 2.000 fechas puede arruinar la interacción. Por eso el objetivo nunca es «reducir el número de renders», sino reducir el tiempo de trabajo por interacción. Con esa distinción clara, las siete técnicas que vienen a continuación se entienden solas: todas eliminan trabajo, no lo aceleran.

  1. Técnica 1: bajar el estado al componente que lo usa

Es la técnica más rentable de todas y no requiere ninguna API nueva. Si un estado está más arriba de donde se necesita, cada cambio reejecuta un subárbol mucho mayor del necesario. La solución es moverlo hacia abajo: es la operación inversa a «elevar el estado» de 04-01, y responde a la misma pregunta —¿quién necesita este dato?— con la respuesta contraria.

// ❌ ANTES: el borrador del formulario vive en la página
function PaginaNuevaReserva() {
  const [horas, setHoras] = useState(1);          // solo lo usa el formulario
  const { data: bicicletas } = useBicicletas();
  const { data: estaciones } = useEstaciones();

  return (
    <Panel titulo="Nueva reserva">
      <ResumenFlota bicicletas={bicicletas} estaciones={estaciones} />
      <MigasDePan />
      <FormularioReserva horas={horas} alCambiarHoras={setHoras} />
    </Panel>
  );
}

Cada pulsación en el campo de horas reejecuta PaginaNuevaReserva, y con ella ResumenFlota —que agrega las 2.000 bicicletas por estado— y MigasDePan. El campo escribe un número y el coste es un recuento completo de la flota.

// ✅ DESPUÉS: el borrador vive donde se usa
function PaginaNuevaReserva() {
  const { data: bicicletas } = useBicicletas();
  const { data: estaciones } = useEstaciones();

  return (
    <Panel titulo="Nueva reserva">
      <ResumenFlota bicicletas={bicicletas} estaciones={estaciones} />
      <MigasDePan />
      <FormularioReserva />           {/* el estado de horas vive dentro */}
    </Panel>
  );
}

function FormularioReserva() {
  const [horas, setHoras] = useState(1);   // el render se detiene aquí
  // …
}

Ahora escribir en el campo reejecuta un solo componente. Sin memo, sin useCallback, sin dependencias que mantener. Y el código es más corto que antes, no más largo: es la señal de que la optimización es estructural y no un parche.

La regla operativa, que ya viste en 07-01 al clasificar el estado: coloca cada estado en el ancestro común más bajo de quienes lo leen, no más arriba.

  1. Técnica 2: pasar children para aislar un subárbol

Cuando el estado no se puede bajar porque lo consume el propio componente que envuelve al resto, queda la segunda técnica estructural, que ya apareció en 04-02 y que aquí revela su verdadera utilidad. El truco está en cómo funciona children: el JSX que se pasa como children se crea en el componente padre, no en el que lo recibe. Si el padre no se reejecuta, esos elementos son literalmente los mismos objetos de antes, y React se salta su render.

// ❌ ANTES: Diseno tiene el estado del panel lateral y crea el contenido
function Diseno() {
  const [lateralAbierto, setLateralAbierto] = useState(false);

  return (
    <div className={estilos.marco}>
      <Cabecera alAlternarLateral={() => setLateralAbierto((v) => !v)} />
      {lateralAbierto && <PanelLateral />}
      <main>
        <Outlet />       {/* toda la página se reejecuta al abrir el lateral */}
      </main>
      <PieDePagina />
    </div>
  );
}

Abrir el panel lateral reejecuta Diseno, y con él <Outlet /> y la página completa que hay debajo: el catálogo con sus 2.000 tarjetas. Un cambio puramente decorativo del marco cuesta un render de toda la aplicación.

// ✅ DESPUÉS: el estado baja a un componente que recibe children
function Diseno() {
  return (
    <MarcoConLateral>
      <main>
        <Outlet />
      </main>
    </MarcoConLateral>
  );
}

function MarcoConLateral({ children }) {
  const [lateralAbierto, setLateralAbierto] = useState(false);

  return (
    <div className={estilos.marco}>
      <Cabecera alAlternarLateral={() => setLateralAbierto((v) => !v)} />
      {lateralAbierto && <PanelLateral />}
      {children}          {/* creado por Diseno, que NO se ha reejecutado */}
      <PieDePagina />
    </div>
  );
}

Ahora setLateralAbierto reejecuta solo MarcoConLateral. La prop children que recibe es el mismo objeto que en el render anterior, React lo detecta por identidad y no baja por esa rama. El catálogo ni se entera.

Esta técnica se llama a veces «memorización estructural»: consigue el efecto de memo sin memo, sin comparación y sin memoria adicional, solo colocando el estado en el sitio correcto del árbol.

  1. Técnica 3: derivar durante el render en vez de guardar y sincronizar

La tercera técnica no reduce renders: reduce el trabajo y elimina una clase entera de errores. Es la misma lección de 05-02 sobre efectos innecesarios, leída ahora en clave de rendimiento.

// ❌ Estado redundante sincronizado con un efecto
function ResumenFlota({ bicicletas }) {
  const [disponibles, setDisponibles] = useState(0);

  useEffect(() => {
    setDisponibles(bicicletas.filter((b) => b.estado === 'disponible').length);
  }, [bicicletas]);

  return <p>{disponibles} bicicletas disponibles</p>;
}

Este componente hace dos renders por cada cambio: uno con el valor viejo y otro tras el efecto. Además, durante un instante muestra un dato incorrecto, y si bicicletas cambia de identidad sin cambiar de contenido, el efecto se dispara igual.

// ✅ Derivado durante el render: un solo render, siempre coherente
function ResumenFlota({ bicicletas }) {
  const disponibles = bicicletas.filter((b) => b.estado === 'disponible').length;
  return <p>{disponibles} bicicletas disponibles</p>;
}

Un solo render, imposible de desincronizar y menos código. La pregunta que surge inmediatamente —«¿y si el cálculo es caro?»— tiene respuesta en 08-03 con useMemo, pero el orden importa: primero se deriva, y solo si se mide que el cálculo pesa se memoriza. Guardar en estado lo que se puede calcular es un problema de corrección antes que de velocidad.

  1. Técnica 4: claves estables en listas

En 03-03 quedó establecido por qué key no es un adorno. Desde la perspectiva del rendimiento, el efecto de una clave mal elegida es brutal y silencioso.

// ❌ El índice como clave
{bicicletasVisibles.map((bicicleta, indice) => (
  <TarjetaBicicleta key={indice} bicicleta={bicicleta} />
))}

// ❌ Peor todavía: clave nueva en cada render
{bicicletasVisibles.map((bicicleta) => (
  <TarjetaBicicleta key={crypto.randomUUID()} bicicleta={bicicleta} />
))}

// ✅ Identidad real y estable del dato
{bicicletasVisibles.map((bicicleta) => (
  <TarjetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
))}
Clave Qué ocurre al reordenar por precio Coste
key={indice} React cree que todos los elementos han cambiado de contenido; actualiza props de todos y conserva el estado en la posición equivocada Muchas escrituras en el DOM + errores de estado
key={crypto.randomUUID()} React destruye y recrea todos los nodos en cada render Catastrófico: DOM completo + efectos remontados
key={bicicleta.id} React mueve los nodos existentes Mínimo, y el estado viaja con su elemento

Con 2.000 tarjetas y el orden de sliceCatalogo cambiando de precio a modelo, la diferencia entre la primera y la tercera opción es la diferencia entre una animación fluida y un congelado de medio segundo. Y no cuesta nada: es escribir la clave correcta.

  1. Técnica 5: retrasar y limitar las entradas de texto

useDebounce ya está en el proyecto desde 05-06 y BuscadorBicicletas lo usa. Merece la pena verlo ahora por lo que es en realidad: una técnica de rendimiento que reduce la frecuencia del trabajo en lugar de reducir su coste.

// src/componentes/BuscadorBicicletas.jsx (recordatorio)
function BuscadorBicicletas() {
  const [texto, setTexto] = useState('');
  const terminoRetrasado = useDebounce(texto, 300);
  const despachar = useDispatch();

  useEffect(() => {
    despachar(terminoCambiado(terminoRetrasado));
  }, [terminoRetrasado, despachar]);

  return (
    <input
      type="search"
      value={texto}                                  // el campo responde a cada tecla
      onChange={(e) => setTexto(e.target.value)}
      aria-label="Buscar bicicletas por modelo"
    />
  );
}

La clave del patrón: el <input> sigue actualizándose en cada pulsación (es un componente barato y su respuesta debe ser inmediata), pero el filtrado de las 2.000 bicicletas ocurre una sola vez, 300 ms después de la última tecla. De 12 filtrados al escribir «eléctrica» se pasa a 1.

Su pariente cercano es el estrangulamiento (throttle), que en lugar de esperar al silencio garantiza como máximo una ejecución cada X milisegundos. Es el adecuado para eventos continuos que no tienen «final»: desplazamiento, redimensionado, movimiento del ratón.

// src/hooks/useThrottle.js
import { useState, useEffect, useRef } from 'react';

export function useThrottle(valor, intervaloMs = 200) {
  const [valorLimitado, setValorLimitado] = useState(valor);
  const ultimaEjecucion = useRef(Date.now());

  useEffect(() => {
    const restante = intervaloMs - (Date.now() - ultimaEjecucion.current);

    if (restante <= 0) {
      ultimaEjecucion.current = Date.now();
      setValorLimitado(valor);
      return;
    }
    const id = setTimeout(() => {
      ultimaEjecucion.current = Date.now();
      setValorLimitado(valor);
    }, restante);

    return () => clearTimeout(id);
  }, [valor, intervaloMs]);

  return valorLimitado;
}

Cómo elegir entre los dos:

useDebounce useThrottle
Cuándo actúa Cuando el usuario para A intervalos regulares mientras ocurre
Garantiza ejecuciones intermedias No
Caso típico Búsqueda, autoguardado, validación remota Desplazamiento, resize, arrastre
En CicloUrbano BuscadorBicicletas useAnchoVentana al redimensionar

  1. Técnica 6: paginar y virtualizar listas largas

Supongamos que CicloUrbano crece y db.json pasa a tener 2.000 bicicletas repartidas por la ciudad — es el escenario que este módulo usará de aquí en adelante. Pintar 2.000 TarjetaBicicleta significa crear del orden de 20.000 nodos del DOM. Ninguna memorización arregla eso, porque el trabajo es real: el navegador tiene que maquetar y pintar todo lo que existe.

Hay dos respuestas, y la primera es casi siempre la mejor:

Paginar. json-server ya soporta _page y _limit, y en 07-06 se preparó la consulta paginada con placeholderData para que la lista no parpadee. Si el usuario ve 24 bicicletas por página, el problema desaparece de raíz: nunca hay más de 24 tarjetas en el DOM, y además se descargan menos datos.

Virtualizar (o «ventana deslizante»). Cuando el diseño exige una lista continua con desplazamiento infinito, la técnica consiste en montar solo los elementos visibles más un pequeño margen, y simular la altura total con un contenedor vacío para que la barra de desplazamiento sea creíble.

flowchart LR
    subgraph SIN["Sin virtualizar"]
        A["2.000 tarjetas en el DOM<br/>~20.000 nodos"]
    end
    subgraph CON["Virtualizado"]
        B["Contenedor con altura total<br/>2.000 x 120px = 240.000px"]
        B --> C["Solo ~14 tarjetas montadas<br/>las visibles + margen"]
        C --> D["Al desplazar: se reutilizan<br/>cambiando datos y posicion"]
    end

La biblioteca estándar hoy es @tanstack/react-virtual, de los mismos autores que TanStack Query, sin dependencias y agnóstica del marco de trabajo.

npm install @tanstack/react-virtual
// src/componentes/ListaBicicletasVirtual.jsx
import { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import TarjetaBicicleta from './TarjetaBicicleta.jsx';
import estilos from './ListaBicicletasVirtual.module.css';

function ListaBicicletasVirtual({ bicicletas }) {
  const contenedorRef = useRef(null);

  const virtualizador = useVirtualizer({
    count: bicicletas.length,               // 1) cuántos elementos hay en total
    getScrollElement: () => contenedorRef.current,  // 2) quién tiene el scroll
    estimateSize: () => 120,                // 3) altura estimada de cada tarjeta, en px
    overscan: 5                             // 4) cuántos montar fuera de la vista
  });

  return (
    <div ref={contenedorRef} className={estilos.ventana}>
      {/* 5) Un div con la altura TOTAL: hace creíble la barra de desplazamiento */}
      <div style={{ height: `${virtualizador.getTotalSize()}px`, position: 'relative' }}>
        {virtualizador.getVirtualItems().map((elemento) => {
          const bicicleta = bicicletas[elemento.index];
          return (
            // 6) Cada tarjeta se posiciona en absoluto en su desplazamiento real
            <div
              key={bicicleta.id}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${elemento.size}px`,
                transform: `translateY(${elemento.start}px)`
              }}
            >
              <TarjetaBicicleta bicicleta={bicicleta} />
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default ListaBicicletasVirtual;

Punto por punto:

  1. count es el número lógico de elementos; el virtualizador nunca los recorre todos.
  2. getScrollElement devuelve el elemento con overflow: auto que genera el desplazamiento.
  3. estimateSize puede ser aproximado: si las alturas varían, se corrige midiendo con measureElement.
  4. overscan monta unos pocos elementos fuera de la vista para que el desplazamiento rápido no muestre huecos.
  5. El contenedor interior tiene la altura total real (2.000 × 120 px = 240.000 px) aunque esté casi vacío.
  6. transform: translateY(...) es preferible a top porque no provoca recálculo de maquetación.

El resultado en cifras: de ~20.000 nodos del DOM a ~140. Ninguna otra técnica de este módulo se acerca a esa mejora. Antes de memorizar una lista larga, pregúntate si debería ser corta.

Advertencias honestas sobre la virtualización, porque no es gratis: rompe la búsqueda del navegador (Ctrl+F no encuentra lo que no está montado), complica la accesibilidad y el enfoque por teclado, exige cuidado con las alturas variables e interfiere con la impresión. Por eso paginar suele ser mejor decisión de producto.

  1. Técnica 7: sacar trabajo del hilo principal

El navegador tiene un solo hilo para ejecutar JavaScript, responder a eventos y pintar. Todo lo que ocupe ese hilo más de ~50 ms se convierte en una interfaz que no responde. Las palancas, de más simple a más compleja:

Palanca Qué hace Cuándo usarla en CicloUrbano
Hacer menos Paginar, filtrar en el servidor, pedir menos campos json-server con _page, _limit y _sort en vez de traer 2.000 bicicletas
Hacerlo una vez Precalcular y guardar en la caché de Query El recuento por estación, calculado en select de la consulta
Hacerlo más tarde useTransition / useDeferredValue (08-03) Filtrar el catálogo mientras el campo sigue respondiendo
Hacerlo fuera Web Worker Un informe de uso mensual sobre miles de reservas
Que lo haga el CSS Animar con transform y opacity, que no tocan la maquetación Transición de apertura del Modal
Que lo haga el navegador content-visibility: auto, loading="lazy" en imágenes Fotos de estaciones bajo el pliegue

Un ejemplo del último caso, que a menudo se pasa por alto porque no es «código React»:

/* src/componentes/ListaBicicletas.module.css */
.tarjeta {
  content-visibility: auto;       /* el navegador no maqueta lo que no se ve */
  contain-intrinsic-size: 0 120px; /* altura reservada para que el scroll no salte */
}

Dos líneas de CSS que evitan que el navegador maquete tarjetas fuera de la pantalla. No es virtualización —los nodos siguen existiendo en el DOM— pero el coste de maquetación y pintado baja mucho, y no requiere ninguna biblioteca.

  1. El React Compiler de React 19: qué automatiza y qué no

React 19 estabilizó el React Compiler, y conviene explicarlo con precisión porque genera dos malentendidos opuestos: quien cree que ya no hace falta aprender memorización, y quien lo ignora por completo.

Qué es. Un compilador que se ejecuta en tiempo de construcción (como un complemento de Babel dentro de Vite), analiza tus componentes y hooks, y inserta automáticamente la memorización que tú habrías escrito a mano con memo, useMemo y useCallback. Convierte el código en una versión equivalente que reutiliza valores, funciones y elementos JSX cuando sus entradas no han cambiado.

Cómo se activa en Vite:

npm install --save-dev babel-plugin-react-compiler
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', { target: '19' }]]
      }
    })
  ]
});

Y la regla de oro para adoptarlo con cabeza: antes de activarlo, pasa el linter de las reglas de React (eslint-plugin-react-hooks incluye las reglas del compilador). El compilador es conservador: si detecta que un componente rompe las reglas de React —muta props, escribe en una variable de módulo durante el render, lee ref.current mientras renderiza— se salta ese componente y lo deja sin optimizar, en silencio. Un proyecto con muchas infracciones obtiene muchas menos mejoras de las que espera.

Qué automatiza y qué no:

Trabajo ¿Lo hace el compilador?
Memorizar el resultado de un cálculo entre renders , el equivalente a useMemo
Estabilizar la identidad de funciones definidas en el componente , el equivalente a useCallback
Evitar reejecutar un componente cuyas props no cambiaron , el equivalente a memo
Estabilizar el value de un proveedor de contexto , si el valor se construye en el propio componente
Reducir el tamaño del paquete JavaScript No. Es cosa de 08-04
Evitar un cálculo que es caro por sí mismo en su primera ejecución No. Ordenar 2.000 bicicletas sigue costando lo que cuesta
Saber que una dependencia externa (una función importada, un objeto de una biblioteca) es estable No. No puede razonar más allá de tu componente
Arreglar un useEffect que se dispara de más No, y sigue siendo tu responsabilidad
Optimizar código que rompe las reglas de React No: lo omite entero
Decidir bajar el estado, pasar children, paginar o virtualizar No. Ninguna decisión estructural de esta lección

Por qué este módulo sigue siendo imprescindible, aunque actives el compilador mañana:

  • Para leer código existente. La inmensa mayoría de proyectos React en producción están llenos de memo, useMemo y useCallback escritos a mano. Sin entenderlos no puedes mantenerlos.
  • Para depurar. Cuando algo va mal con el compilador activado, el diagnóstico exige saber exactamente qué memorización se esperaba y cuál no llegó.
  • Porque muchos proyectos no lo adoptarán. Bases de código grandes, versiones antiguas de React, cadenas de construcción que no usan Babel.
  • Porque no elimina el criterio. El compilador memoriza; no decide qué merece la pena calcular, qué debería estar paginado ni dónde debe vivir el estado. Ese sigue siendo el trabajo difícil.
  • Porque memorizar no es optimizar. Si el problema es que descargas 1,2 MB de JavaScript o que pides 2.000 registros, el compilador no ayuda en nada.

El React Compiler elimina el trabajo mecánico de memorizar. No elimina la necesidad de entender qué se memoriza, por qué y cuándo eso no es la solución.

  1. Presupuesto de rendimiento y flujo de trabajo

Un presupuesto de rendimiento convierte «esto va lento» en una condición verificable. Sin él no hay forma de saber cuándo hay que optimizar ni, sobre todo, cuándo hay que parar. Este es un presupuesto razonable para CicloUrbano:

Magnitud Presupuesto Cómo se mide
JavaScript inicial (comprimido) < 200 KB Salida de npm run build (08-04)
LCP en móvil de gama media < 2,5 s Lighthouse en modo móvil
INP al escribir en el buscador < 200 ms Profiler + pestaña Rendimiento (08-05)
Duración del commit al filtrar el catálogo < 16 ms React DevTools Profiler (08-05)
Tarjetas montadas simultáneamente < 60 Inspector de elementos

Y este es el flujo de trabajo que ordena todo el módulo. Léelo como un bucle, no como una lista:

flowchart TD
    A["1. Definir el presupuesto<br/>y la interaccion concreta"] --> B["2. MEDIR en build de produccion<br/>Profiler + Lighthouse"]
    B --> C{"Se incumple<br/>el presupuesto?"}
    C -->|No| Z["PARAR<br/>no hay nada que optimizar"]
    C -->|Si| D["3. Localizar el cuello de botella<br/>que componente, que commit, por que"]
    D --> E{"Que tipo de<br/>problema es?"}
    E -->|"Descarga inicial"| F["Division de codigo 08-04"]
    E -->|"Demasiados elementos"| G["Paginar / virtualizar"]
    E -->|"Estado mal colocado"| H["Bajar estado / children"]
    E -->|"Frecuencia excesiva"| I["Debounce / transiciones"]
    E -->|"Renders con props iguales"| J["memo 08-02"]
    E -->|"Calculo caro o identidad"| K["useMemo / useCallback 08-03"]
    F --> L["4. Aplicar la tecnica MAS SIMPLE<br/>una sola por vez"]
    G --> L
    H --> L
    I --> L
    J --> L
    K --> L
    L --> M["5. VOLVER A MEDIR"]
    M --> N{"Mejoro de forma<br/>perceptible?"}
    N -->|Si| B
    N -->|No| O["Revertir el cambio<br/>y volver al paso 3"]
    O --> D

Dos detalles del diagrama que no son decorativos:

  • «Aplicar una sola técnica por vez». Si aplicas memo, useCallback y virtualización a la vez y mejora, no sabes cuál sirvió; probablemente estés cargando con dos optimizaciones inútiles para siempre.
  • «Revertir el cambio». Una optimización que no mejora de forma medible debe deshacerse. Su coste de legibilidad y memoria sigue ahí aunque el beneficio no exista.

  1. Mapa del resto del módulo

Lección Herramienta Problema que resuelve
08-02 React.memo Un componente se reejecuta aunque sus props sean idénticas
08-03 useMemo, useCallback, useTransition, useDeferredValue Un cálculo es caro, o un valor cambia de identidad y estropea todo lo demás
08-04 React.lazy, Suspense, import() El navegador descarga código que todavía no necesita
08-05 React DevTools Profiler, <Profiler> Saber qué está pasando de verdad, que es el paso 2 de todos los anteriores

Errores Comunes y Consejos

Optimizar en modo de desarrollo. El servidor de Vite, los avisos de React y el doble render de StrictMode distorsionan cualquier medición. Todo lo que se mida en serio se mide con npm run build y npm run preview (08-05).

Perseguir el número de renders como objetivo. «He bajado de 340 renders a 12» no es un logro si la interacción tardaba 30 ms y sigue tardando 28. La métrica es el tiempo, no la cuenta.

Empezar por la memorización. Es el orden invertido. Cuando se llega a memo habiendo descartado bajar el estado, children, derivar, paginar y virtualizar, el memo suele sobrar. Cuando se empieza por memo, casi siempre se acaba con un proyecto lleno de memorización y el problema real intacto.

Consejo: mide en un dispositivo lento. Las herramientas del navegador permiten ralentizar la CPU 4× o 6× (CPU throttling). Un problema que en tu portátil es invisible se vuelve obvio con 4×, que es aproximadamente un móvil de gama media.

Consejo: pon el problema en su capa. Antes de tocar React, comprueba el tamaño de las imágenes, el número de peticiones, si el servidor pagina y si las tipografías bloquean el pintado. Muchas «aplicaciones React lentas» son aplicaciones con 3 MB de imágenes.

Consejo: escribe el presupuesto en el repositorio. Un fichero RENDIMIENTO.md con las cinco cifras del apartado 14 convierte una discusión de opiniones en una comprobación objetiva, y sobrevive a los cambios de equipo.

Consejo: activa las reglas del linter antes que el compilador. eslint-plugin-react-hooks con las reglas del React Compiler te dice qué componentes rompen las reglas de React. Arreglarlos mejora el código aunque nunca actives el compilador, y es requisito para que este sirva de algo.

Ejercicios

Ejercicio 1. El siguiente componente de CicloUrbano hace que escribir en el campo de notas repinte todo el panel de reservas, incluido PanelReservas, que agrega el historial completo del usuario. Identifica el problema, di qué técnica de esta lección lo resuelve y reescribe el componente.

function PaginaReservas() {
  const [nota, setNota] = useState('');
  const { data: reservas } = useReservas();

  return (
    <Panel titulo="Mis reservas">
      <PanelReservas reservas={reservas} />
      <ResumenFlota />
      <label>
        Nota interna
        <input value={nota} onChange={(e) => setNota(e.target.value)} />
      </label>
      <button onClick={() => guardarNota(nota)}>Guardar nota</button>
    </Panel>
  );
}

Ejercicio 2. Para cada síntoma de CicloUrbano, indica qué métrica del apartado 3 empeora y qué técnica de esta lección (o qué lección del módulo) es la adecuada. No uses memo, useMemo ni useCallback en ninguna respuesta.

# Síntoma
a La primera visita tarda 4,1 s en mostrar el catálogo en 4G
b Al cambiar el orden del catálogo de precio a modelo, la pantalla se congela 600 ms
c Cada tecla en el buscador provoca una petición a json-server
d Al terminar de cargar las fotos de estaciones, la lista da un salto y el usuario pulsa donde no quería
e Desplazarse por las 2.000 bicicletas va a tirones en el móvil

Ejercicio 3. Un compañero propone activar el React Compiler y borrar todos los memo, useMemo y useCallback del proyecto «porque ahora son automáticos». Enumera al menos cuatro objeciones técnicas concretas y describe qué comprobación harías antes de aceptar o rechazar la propuesta.

Soluciones

Solución 1. El problema es estado mal colocado: nota solo lo usan el <input> y el botón, pero vive en PaginaReservas, así que cada pulsación reejecuta PanelReservas y ResumenFlota. La técnica es la 1, bajar el estado (apartado 6), extrayendo el campo y su botón a un componente propio.

function PaginaReservas() {
  const { data: reservas } = useReservas();

  return (
    <Panel titulo="Mis reservas">
      <PanelReservas reservas={reservas} />
      <ResumenFlota />
      <CampoNotaInterna />        {/* el estado se detiene aquí */}
    </Panel>
  );
}

function CampoNotaInterna() {
  const [nota, setNota] = useState('');

  return (
    <>
      <label>
        Nota interna
        <input value={nota} onChange={(e) => setNota(e.target.value)} />
      </label>
      <button onClick={() => guardarNota(nota)}>Guardar nota</button>
    </>
  );
}

Ahora escribir reejecuta solo CampoNotaInterna. No hace falta memo en PanelReservas ni en ResumenFlota: React ni siquiera baja por esa rama. Es la lección estructural del módulo: la mejor optimización es la que hace innecesaria la optimización.

Solución 2.

# Métrica Técnica
a LCP / FCP (y TTI) División de código y carga perezosa (08-04), más paginar la consulta inicial
b INP / TBT Comprobar primero las claves estables (técnica 4): con key={indice} una reordenación reescribe el DOM entero. Después, paginar o virtualizar (técnica 6) y, si hace falta, useTransition (08-03)
c INP y carga del servidor useDebounce (técnica 5), ya presente en BuscadorBicicletas; combinado con el staleTime de la consulta
d CLS Reservar el espacio: width/height o aspect-ratio en las imágenes, y esqueletos con la altura final en vez de un texto «Cargando…» (se retoma en 08-04)
e INP / TBT y memoria Paginar o virtualizar (técnica 6) con @tanstack/react-virtual, y content-visibility: auto como medida inmediata (técnica 7)

Ninguna de las cinco se resuelve memorizando. Ese es exactamente el objetivo del ejercicio.

Solución 3. Objeciones:

  1. El compilador no memoriza lo que no puede analizar. Si un componente rompe las reglas de React, lo omite en silencio; al borrar la memorización manual, esos componentes quedan peor que antes y sin ningún aviso.
  2. No cubre las dependencias externas. Una función importada de una utilidad o un objeto de opciones creado fuera del componente siguen necesitando criterio humano; el compilador razona dentro del componente.
  3. No hace baratos los cálculos caros. Ordenar y filtrar 2.000 bicicletas cuesta lo mismo la primera vez; el compilador evita repetirlo, no evita hacerlo.
  4. Rompe el código para quien no lo tenga activado. Si parte del proyecto se extrae a una biblioteca compartida, o si otro equipo compila sin el complemento, la memorización desaparece.
  5. Elimina la documentación implícita. Un useMemo con dependencias explícitas comunica una intención de diseño que el código sin él ya no expresa.
  6. Es un cambio irreversible en la práctica. Volver a introducir la memorización manual en cientos de componentes cuesta mucho más que dejarla.

Comprobación previa: medir antes y después con el Profiler (08-05) sobre las interacciones críticas —escribir en el buscador, cambiar el orden, abrir el DialogoReserva— en un build de producción, con y sin el compilador, y con el linter de reglas de React en verde. Si las cifras son equivalentes, el compilador está haciendo su trabajo y la retirada de memorización manual puede plantearse gradualmente, componente a componente, no en una sola operación.

Conclusión

Este módulo empieza por el método porque sin método la optimización es superstición. La regla que lo gobierna todo es medir primero: si no puedes señalar un número que incumple un presupuesto, no tienes un problema, tienes una sospecha, y actuar sobre sospechas cuesta legibilidad, memoria y —lo más grave— errores de dependencias que hacen que la aplicación muestre datos incorrectos.

«Lento» se ha convertido en magnitudes concretas: FCP y LCP dependen sobre todo del tamaño del paquete y del servidor; INP y TBT son el territorio propio de React y de este módulo; CLS se arruina con indicadores de carga mal diseñados. Y se ha fijado el modelo causal: un componente se vuelve a ejecutar por cuatro razones —su estado, un ancestro, un contexto que consume, un almacén externo suscrito—, entre las que no está «le cambiaron las props». De ahí el malentendido más caro del ecosistema, ya desactivado: volver a renderizar no es volver a pintar. Un render es ejecutar una función y comparar el resultado; el DOM solo se toca donde algo cambió. Lo caro no son los renders, es el trabajo por interacción.

Con eso claro, las siete técnicas que casi siempre rinden más que la memorización: bajar el estado al componente que lo usa (la más rentable de todas, y encima acorta el código), pasar children para que un subárbol conserve su identidad y React no baje por esa rama, derivar durante el render en lugar de guardar y sincronizar con un efecto, claves estables en las listas —donde un key={indice} convierte una reordenación en una reescritura completa del DOM—, useDebounce y useThrottle para reducir la frecuencia del trabajo, paginar o virtualizar con @tanstack/react-virtual cuando el catálogo crece a 2.000 bicicletas, y sacar trabajo del hilo principal con precálculo, CSS y content-visibility. Ninguna necesita memo.

El React Compiler de React 19 se ha presentado sin exageraciones: automatiza la memorización mecánica que habrías escrito a mano, no reduce el tamaño del paquete, no abarata un cálculo caro, no razona sobre dependencias externas, omite en silencio el código que rompe las reglas de React y no toma ninguna decisión estructural. Sigue haciendo falta entender la memorización manual para leer el código que existe, para depurar y para los proyectos que no lo adopten. Y el flujo de trabajo que ordena el módulo: medir → localizar → aplicar la técnica más simple, una sola cada vez → volver a medir → revertir si no mejora.

Ahora sí llega el turno de las herramientas. La primera es la que responde a la causa 2 de la lista —«un ancestro se ha vuelto a renderizar»— cuando ya no queda margen estructural para evitarlo: envolver un componente para que React compare sus props y se salte su ejecución si son las mismas. La próxima lección es Memorización con React.memo.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados