En esta página

Rendimiento y Optimización

12 min lectura TextoCap. 5 — Producción

Medir antes de optimizar

La primera regla del rendimiento en React es nunca optimizar sin medir. Agregar useMemo, useCallback y React.memo a ciegas puede incluso empeorar el rendimiento por el overhead adicional.

El flujo correcto es:

  1. Identificar el problema con React DevTools Profiler
  2. Entender la causa raíz del render innecesario
  3. Aplicar la solución mínima necesaria
  4. Verificar que la mejora es real con el Profiler

React DevTools Profiler

El Profiler de React DevTools (extensión del navegador) muestra:

  • Qué componentes se renderizaron en cada interacción
  • Cuánto tiempo tardó cada componente
  • Por qué se renderizó (qué prop/estado cambió)
import { Profiler } from 'react';

// Profiler programático para producción o CI
<Profiler
  id="MiComponente"
  onRender={(id, fase, duracionReal) => {
    // Enviar a analytics si el render es muy lento
    if (duracionReal > 50 && process.env.NODE_ENV === 'production') {
      void analytics.track('slow_render', { id, duracionReal, fase });
    }
  }}
>
  <MiComponente />
</Profiler>

React Compiler — memoización automática

El React Compiler (activado por defecto en nuevos proyectos con React 19.2) transforma tu código para agregar optimizaciones automáticamente:

# Instalar el compilador
npm install babel-plugin-react-compiler

# babel.config.js
module.exports = {
  plugins: [['babel-plugin-react-compiler', {}]],
};
// Con React Compiler, esto es igual de eficiente que con useMemo/useCallback manual:
function Componente({ lista, filtro }: Props) {
  // El compilador detecta que esto depende de lista y filtro
  const filtrada = lista.filter((item) => item.includes(filtro));

  // El compilador detecta que esto no cambia entre renders del padre
  const manejarClic = () => console.log('clic');

  return <Lista items={filtrada} onClick={manejarClic} />;
}

Code splitting con React.lazy y Vite

Dividir el código por rutas es la optimización de mayor impacto:

import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';

// Cada import() crea un chunk separado
const Inicio = lazy(() => import('./pages/Inicio'));
const Cursos = lazy(() => import('./pages/Cursos'));
const DetalleCurso = lazy(() => import('./pages/DetalleCurso'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Admin = lazy(() => import('./pages/Admin'));

// Wrapper de Suspense para todas las rutas lazy
function PaginaLazy({ children }: { children: React.ReactNode }) {
  return (
    <Suspense fallback={<div className="page-skeleton" aria-busy="true" />}>
      {children}
    </Suspense>
  );
}

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <PaginaLazy><Inicio /></PaginaLazy> },
      { path: 'cursos', element: <PaginaLazy><Cursos /></PaginaLazy> },
      { path: 'cursos/:id', element: <PaginaLazy><DetalleCurso /></PaginaLazy> },
      { path: 'dashboard', element: <PaginaLazy><Dashboard /></PaginaLazy> },
      { path: 'admin', element: <PaginaLazy><Admin /></PaginaLazy> },
    ],
  },
]);

En un proyecto típico de 1MB de JS, el code splitting puede reducir el bundle inicial a 200-300KB.

Optimización de imágenes

// Usa loading="lazy" para imágenes fuera del viewport inicial
function GaleriaProductos({ productos }: { productos: Producto[] }) {
  return (
    <div className="galeria">
      {productos.map((p, i) => (
        <img
          key={p.id}
          src={p.imagenUrl}
          alt={p.nombre}
          width={300}
          height={200}
          // Primera imagen: eager (crítica para LCP)
          // El resto: lazy (no bloquean la carga inicial)
          loading={i === 0 ? 'eager' : 'lazy'}
          decoding="async"
        />
      ))}
    </div>
  );
}

Evitar re-renders con selección granular de estado

Con Context API o Zustand, los selectores son claves para el rendimiento:

// ❌ Se re-renderiza cuando cualquier cosa en el store cambia
function ComponenteLento() {
  const { usuario, tema, idioma, notificaciones, carrito } = useAppStore();
  return <span>{usuario.nombre}</span>;
}

// ✅ Solo se re-renderiza cuando usuario.nombre cambia
function ComponenteEficiente() {
  const nombre = useAppStore((s) => s.usuario.nombre);
  return <span>{nombre}</span>;
}

Identificar y eliminar renders en cascada

Un patrón problemático es el render en cascada: un cambio de estado en el padre re-renderiza todos sus hijos aunque no les afecte.

// ❌ Problema: el estado del filtro está en el padre y causa re-render de toda la lista
function ListaConFiltro({ items }: { items: Item[] }) {
  const [filtro, setFiltro] = useState('');
  const filtrados = items.filter((i) => i.nombre.includes(filtro));

  return (
    <>
      <input value={filtro} onChange={(e) => setFiltro(e.target.value)} />
      <Lista items={filtrados} /> {/* Re-renderiza en cada keystroke */}
    </>
  );
}

// ✅ Solución: mover el estado al componente más específico
function FiltroInput({ onCambio }: { onCambio: (v: string) => void }) {
  const [filtro, setFiltro] = useState('');
  const manejar = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFiltro(e.target.value);
    onCambio(e.target.value);
  };
  return <input value={filtro} onChange={manejar} />;
}

La optimización de rendimiento en React es un proceso iterativo: mide, identifica, aplica la solución mínima y verifica. Con el React Compiler disponible, muchas optimizaciones manuales se vuelven innecesarias en proyectos nuevos.

React Compiler elimina la memoización manual en proyectos nuevos
Con React Compiler (activado en React 19.2 con babel-plugin-react-compiler), el compilador analiza tu código y añade automáticamente la memoización donde es necesaria. Esto significa que useMemo, useCallback y React.memo pueden omitirse en la mayoría de los casos.
Code splitting por ruta reduce el bundle inicial drásticamente
Implementar React.lazy() en cada ruta de tu router puede reducir el bundle inicial en un 40-60% en aplicaciones grandes. Vite genera automáticamente chunks separados por cada import() dinámico, y el navegador los descarga solo cuando el usuario navega a esa ruta.
La virtualización es necesaria solo para listas de más de 200 elementos
Para listas de menos de 200 items, la virtualización añade complejidad innecesaria. Considera primero la paginación o infinite scroll con TanStack Query antes de virtualizar. La virtualización dificulta el acceso por teclado y puede necesitar ajustes de accesibilidad adicionales.