En esta página
Rendimiento y Optimizació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:
- Identificar el problema con React DevTools Profiler
- Entender la causa raíz del render innecesario
- Aplicar la solución mínima necesaria
- 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.
Inicia sesión para guardar tu progreso