Conceptos básicos de rendimiento
13 ejemplos para comenzar con React Performance -- 9 básicos y 4 intermedios.
Busca en todas las páginas de la documentación
13 ejemplos para comenzar con React Performance -- 9 básicos y 4 intermedios.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Todos los ejemplos asumen un proyecto Next.js 15+ App Router con React 19 y TypeScript. Algunos ejemplos utilizan herramientas adicionales:
@next/bundle-analyzer para inspección de bundles: npm install --save-dev @next/bundle-analyzer.web-vitals para monitoreo de Core Web Vitals: npm install web-vitals.zustand para el ejemplo de rendimiento de estado: npm install zustand.Dos reglas orientadoras para cada ejemplo a continuación:
memo, useMemo, useCallback manuales solo después de que el profiling demuestre la ganancia.¿Buscas una revisión sistemática? Consulta la Performance Checklist -- una auditoría de 30 puntos que puedes ejecutar en CI.
Usa React DevTools para registrar una interacción real y ver qué componentes se renderizaron realmente.
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
console.log(`[${id}] ${phase} took ${actualDuration.toFixed(1)}ms`);
};
export default function Page() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}
function Dashboard() {
return <p>Content</p>;
}<Profiler> integrado registra tiempos de renderizado para su subárbol -- excelente para mediciones puntuales.actualDuration vs baseDuration -- grandes deltas entre ellos destacan re-renderizados desperdiciados que puedes memoizar.<Profiler> a producción -- tiene una sobrecarga medible. Envuelve su uso en una verificación de solo desarrollo si permanece en el árbol.Related: React DevTools Profiler -- gráficos de llamas, inspección de commits, interacciones | Performance Checklist -- qué medir en CI
Un re-renderizado del padre no tiene que extenderse en cascada -- divide el estado y mantén referencias estables para que los hijos omitan el trabajo.
"use client";
import { memo, useState } from "react";
const Row = memo(function Row({ label }: { label: string }) {
console.log("Render", label);
return <li>{label}</li>;
});
export default function List({ items }: { items: string[] }) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<ul>
{items.map((i) => (
<Row key={i} label={i} />
))}
</ul>
</>
);
}memo(Component) omite re-renderizados cuando los props son referencialmente iguales al último renderizado.List, pero los props de Row (label) no han cambiado, por lo que cada fila se omite.style={{ ... }}) y las funciones en línea rompen memo -- su referencia cambia cada renderizado.key en listas para permitir que React reutilice nodos DOM cuando los elementos se mueven -- un key={index} en una lista reordenada es una fuente de errores frecuente.Related: Preventing Unnecessary Re-renders -- división de estado, elevación, claves | Memoization -- las primitivas detrás de este patrón
Estabiliza valores costosos y referencias de funciones para que los hijos memoizados realmente se beneficien.
"use client";
import { memo, useCallback, useMemo, useState } from "react";
const ExpensiveChart = memo(function ExpensiveChart({
points,
onPick,
}: {
points: number[];
onPick: (n: number) => void;
}) {
return <p>{points.length} points</p>;
});
export default function Dashboard({ raw }: { raw: number[] }) {
const [picked, setPicked] = useState<number | null>(null);
const points = useMemo(() => raw.filter((n) => n > 0), [raw]);
const onPick = useCallback((n: number) => setPicked(n), []);
return (
<>
<p>Picked: {picked ?? "none"}</p>
<ExpensiveChart points={points} onPick={onPick} />
</>
);
}useMemo(fn, deps) cachea el valor; useCallback(fn, deps) cachea la referencia de la función -- ambos mantienen la identidad de props estable para hijos memoizados.raw.filter(...) y (n) => setPicked(n) serían nuevas referencias cada renderizado, invalidando memo.Related: Memoization -- cuándo la memoización realmente ayuda | useMemo / useCallback -- las APIs de hooks
Activa el compilador de React y deja que inserte memoización para ti -- elimina la mayoría de memo/useMemo/useCallback manuales.
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default config;// Después de que el compilador esté activado, escribe componentes simples:
function ProductList({ products }: { products: Product[] }) {
const total = products.reduce((sum, p) => sum + p.price, 0);
const handleClick = (id: string) => console.log(id);
return <p>{total} ({products.length} items)</p>;
}useMemo/useCallback existente sigue funcionando.npx react-compiler-healthcheck valida tu codebase.Related: React Compiler -- configuración, bailouts, depuración | React Compiler (React 19) -- la página de características de React 19
Aplaza la carga de un componente pesado hasta que el usuario realmente lo necesite.
"use client";
import dynamic from "next/dynamic";
import { useState } from "react";
const Chart = dynamic(() => import("./Chart"), {
loading: () => <p>Loading chart...</p>,
ssr: false,
});
export default function Dashboard() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Show chart</button>
{open && <Chart />}
</>
);
}next/dynamic devuelve un componente que se divide en un chunk separado; se descarga solo cuando se renderiza.open && <Chart />) para que el chunk se obtenga bajo demanda -- ideal para modales, gráficos y editores de texto enriquecido.ssr: false se excluye del renderizado del servidor cuando el componente depende de APIs solo del navegador (window, document).Related: Bundle Size Optimization -- analizadores, tree-shaking, costo de paquete | Image & Font Performance -- otras formas de ahorrar bytes
Por defecto, usa Server Components; elimina "use client" solo donde comienza la interactividad.
// app/dashboard/page.tsx -- un Server Component
import { ClientChart } from "./chart";
interface Product {
id: number;
name: string;
price: number;
}
export default async function DashboardPage() {
const res = await fetch("https://api.example.com/products");
const products: Product[] = await res.json();
const total = products.reduce((sum, p) => sum + p.price, 0);
return (
<>
<h1>Total: ${total}</h1>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
<ClientChart data={products} />
</>
);
}<ClientChart> interactivo es el único JS enviado.Related: Server Component Performance -- límites, patrones, mediciones | Server Components (React 19) -- la primitiva
Usa <Image> de Next.js para que el navegador envíe el tamaño correcto, cargue perezosamente por debajo del pliegue y reserve espacio para evitar cambios de diseño.
import Image from "next/image";
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Landing hero"
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
);
}next/image sirve formatos modernos (AVIF/WebP), genera múltiples tamaños y carga perezosamente automáticamente.width y height reservan espacio -- cero cambio de diseño incluso antes de que llegue la imagen.priority desactiva la carga perezosa para imágenes por encima del pliegue para que comiencen a cargarse inmediatamente (gran ganancia de LCP).sizes le dice al navegador qué tan ancha será la imagen en cada punto de ruptura -- necesario para elegir la fuente correcta.Related: Image & Font Performance -- carga de fuentes, precarga, imágenes OG | next/image -- API completa de
<Image>
Envía métricas LCP, INP y CLS de usuarios reales a analytics para que sepas qué ve el mundo real.
// app/web-vitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric.name, metric.value);
// fetch("/api/metrics", { method: "POST", body: JSON.stringify(metric) });
});
return null;
}
// app/layout.tsx
// <WebVitals />useReportWebVitals se dispara una vez por métrica por página -- reenvíalas a tu analytics, Sentry o Vercel Analytics.Related: Core Web Vitals Optimization -- correcciones por métrica y medición | Performance Checklist -- puertas CI y presupuestos
Divide una ruta en límites de transmisión para que los paneles rápidos se rendericen mientras los lentos aún se están obteniendo.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { FastStats } from "./fast-stats";
import { SlowChart } from "./slow-chart";
export default function Dashboard() {
return (
<div>
<Suspense fallback={<p>Loading stats...</p>}>
<FastStats />
</Suspense>
<Suspense fallback={<p>Loading chart...</p>}>
<SlowChart />
</Suspense>
</div>
);
}<Suspense> se transmite independientemente -- el usuario ve el panel rápido inmediatamente en lugar de esperar a que se cargue toda la ruta.loading.tsx para transmisión a nivel de ruta, y <Suspense> anidado para transmisión granular por panel.Promise.all) para que cada panel se cargue completamente en el momento en que su JS se transmita.Related: Suspense & Streaming Performance -- estrategia de límites | Suspense (patterns) -- la primitiva | Streaming (Next.js Data) -- transmisión a nivel de ruta
Suscríbete a un slice de la tienda para que solo los componentes que leen ese slice se re-rendericen.
"use client";
import { create } from "zustand";
interface CartStore {
items: { id: string; qty: number }[];
count: number;
addItem: (id: string) => void;
}
const useCart = create<CartStore>((set) => ({
items: [],
count: 0,
addItem: (id) =>
set((s) => {
const items = [...s.items, { id, qty: 1 }];
return { items, count: items.length };
}),
}));
// Solo se re-renderiza cuando `count` cambia, no cuando cambia cualquier otro campo
function CartBadge() {
const count = useCart((s) => s.count);
return <span>{count}</span>;
}
// Solo se re-renderiza cuando la referencia de `addItem` cambia (nunca, a menos que la tienda se recree)
function AddButton({ id }: { id: string }) {
const addItem = useCart((s) => s.addItem);
return <button onClick={() => addItem(id)}>Add</button>;
}shallow o devuelve primitivos.Related: State Management Performance -- división de contexto, estado derivado | Zustand Selectors -- patrones selectores más profundos
Inicia obtenciones independientes en paralelo para que el tiempo total de espera sea el más lento, no la suma.
// app/dashboard/page.tsx
interface User { name: string; }
interface Stats { totalSales: number; }
interface Activity { events: string[]; }
async function getUser(): Promise<User> {
return (await fetch("https://api.example.com/me")).json();
}
async function getStats(): Promise<Stats> {
return (await fetch("https://api.example.com/stats")).json();
}
async function getActivity(): Promise<Activity> {
return (await fetch("https://api.example.com/activity")).json();
}
export default async function Dashboard() {
const [user, stats, activity] = await Promise.all([
getUser(),
getStats(),
getActivity(),
]);
return (
<p>
{user.name} - ${stats.totalSales} - {activity.events.length} events
</p>
);
}await secuenciales crean una cascada -- cada solicitud espera a que se resuelva la anterior.Promise.all dispara todas las solicitudes a la vez; el tiempo total es igual a la solicitud más lenta.Promise.allSettled cuando un fallo no debe rechazar todo el conjunto (por ejemplo, datos de barra lateral opcionales).Related: Data Fetching Performance -- cascadas, paralelismo, caché | Parallel Promises -- patrones Promise.all, allSettled
Siempre desmonta suscripciones, temporizadores y oyentes en el useEffect cleanup.
"use client";
import { useEffect, useState } from "react";
export default function LiveCounter() {
const [n, setN] = useState(0);
useEffect(() => {
const controller = new AbortController();
const id = setInterval(() => setN((x) => x + 1), 1000);
window.addEventListener("resize", () => console.log("resize"), {
signal: controller.signal,
});
return () => {
clearInterval(id);
controller.abort(); // elimina el oyente
};
}, []);
return <p>Ticks: {n}</p>;
}setInterval, setTimeout, addEventListener y suscripción debe tener una limpieza correspondiente, o el navegador mantiene el componente para siempre.AbortController es la forma moderna de eliminar oyentes -- un abort() desmonta cada oyente registrado con la señal.fetch en vuelo con la misma señal evita advertencias "setState después del desmontaje".Related: Memory Leaks -- detección, patrones WeakMap, refs de larga vida | useEffect -- reglas de limpieza y temporización
Cachea obtenciones costosas entre solicitudes e invalídalas quirúrgicamente cuando los datos cambian.
// app/lib/data.ts - servidor-solo helper
import { unstable_cache } from "next/cache";
interface Post {
id: number;
title: string;
}
export const getPosts = unstable_cache(
async (): Promise<Post[]> => {
const res = await fetch("https://api.example.com/posts");
return res.json();
},
["posts"],
{ tags: ["posts"], revalidate: 300 },
);
// app/posts/actions.ts
"use server";
import { revalidateTag } from "next/cache";
export async function createPost(title: string) {
await fetch("https://api.example.com/posts", {
method: "POST",
body: JSON.stringify({ title }),
});
revalidateTag("posts");
}unstable_cache(fn, keys, options) memoiza el resultado entre solicitudes -- los llamadores posteriores obtienen un acierto de caché.tags: ["posts"] te permite invalidar cada entrada en caché con esa etiqueta en una llamada a través de revalidateTag("posts").revalidate: 300 añade un techo de tiempo de 5 minutos en caso de que olvides etiqueta-invalidar en algún lugar.fetch simple ya memoiza -- reserva unstable_cache para compartir entre solicitudes.Related: Next.js Caching Deep Dive -- modelo de cuatro capas, ciclos de vida del caché | Caching (Next.js Data) -- opciones de caché a nivel de obtención | Revalidation -- revalidatePath vs revalidateTag vs ISR
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥