React Performance
Patrones de optimización de rendimiento para React 18/19 y Next.js, adaptados de Vercel Labs react-best-practices (MIT, v1.0.0). Este skill organiza las reglas por prioridad y proporciona guías de decisión para la revisión y refactorización activa de código.
Cuándo activarlo
- Al escribir o revisar código React/Next.js en busca de rendimiento
- Al diagnosticar cargas lentas de páginas, interacciones lentas o alto uso de CPU en el cliente
- Al auditar el tamaño del bundle o regresiones en Lighthouse Core Web Vitals
- Al eliminar cascadas (waterfalls) en Server Components / rutas API
- Al reducir re-renderizados en el cliente
- Al optimizar listas largas, animaciones o hidratación
- Al auditar decisiones de optimización en PRs que toquen
app/,pages/,components/o capas de datos
Índice de prioridad
| Prioridad | Categoría | Prefijo | Cuándo importa |
|---|---|---|---|
| 1 — CRÍTICO | Eliminación de cascadas | async- | Cada vez que await es seguido de un await independiente |
| 2 — CRÍTICO | Optimización del tamaño del bundle | bundle- | JS de carga inicial, importaciones a nivel de ruta, librerías de terceros |
| 3 — ALTO | Rendimiento del lado del servidor | server- | RSC, Server Actions, rutas API, SSR |
| 4 — MEDIO-ALTO | Obtención de datos en el cliente | client- | SWR / TanStack Query / fetch crudo en hooks |
| 5 — MEDIO | Optimización de re-renderizados | rerender- | Actualizaciones de estado de alta frecuencia, propagación padre-hijo |
| 6 — MEDIO | Rendimiento de renderizado | rendering- | Listas largas, animaciones, hidratación |
| 7 — BAJO-MEDIO | Rendimiento de JavaScript | js- | Bucles intensivos, asignaciones frecuentes |
| 8 — BAJO | Patrones avanzados | advanced- | Integración effect-event, referencias estables |
1. Eliminación de cascadas (CRÍTICO)
"Las cascadas son el asesino número uno del rendimiento" — cada
awaitsecuencial añade latencia de red completa.
Condiciones baratas antes de await
Verifica condiciones síncronas (props, env, flags hardcodeadas) antes de esperar datos remotos.
// INCORRECTO
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// CORRECTO — cortocircuitar con condición síncrona barata primero
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}
Retrasar awaits hasta ser usados
Mueve await dentro de la rama que lo usa.
// INCORRECTO — espera antes de decidir si necesita los datos
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// CORRECTO
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);
Promise.all para trabajo independiente
// INCORRECTO — secuencial
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// CORRECTO — paralelo
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);
Dependencias parciales — empezar temprano, esperar tarde
// CORRECTO — lanzar todas las promesas, esperar solo cuando se necesita cada resultado
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);
Suspense para streaming
Coloca los límites de <Suspense> cerca de los datos para que la página pinte lo que puede mientras los subárboles más lentos se transmiten. La compensación: cambio de diseño cuando llega el contenido — reserva espacio (skeleton o min-height).
Server Components: paralelismo mediante composición
// INCORRECTO — los awaits hermanos se ejecutan secuencialmente dentro de un componente
export default async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// CORRECTO — dividir en hijos, React los ejecuta en paralelo
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}
2. Optimización del tamaño del bundle (CRÍTICO)
Importaciones directas, no barriles
Los archivos barril index.ts fuerzan al bundler a recorrer todo el grafo de módulos aunque tree-shaking elimine la mayor parte. Las importaciones directas ahorran 200-800ms de JS de carga inicial en muchas aplicaciones reales.
// INCORRECTO
import { Button, Card, Modal } from "@/components";
// CORRECTO
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";
Next.js 13.5+ tiene Optimize Package Imports que automatiza esto para paquetes listados — úsalo; las importaciones directas manuales siguen siendo necesarias para librerías no listadas.
Rutas analizables estáticamente
// INCORRECTO — derrota el análisis del bundler/trace
const mod = await import(`./pages/${name}`);
// CORRECTO — explícito por rama
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
Importaciones dinámicas para componentes pesados
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // cuando es solo cliente
});
Diferir scripts de terceros
Carga analíticas, logs, widgets de soporte DESPUÉS de la hidratación. Usa next/script con strategy="afterInteractive" (por defecto) o "lazyOnload".
Carga condicional de módulos
if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}
Precarga al pasar el ratón/enfocar
Activa <link rel="preload"> o import() al pasar el ratón para que el bundle esté en caché cuando el usuario haga clic.
3. Rendimiento del lado del servidor (ALTO)
Autenticar Server Actions como rutas API
Toda función "use server" es un endpoint público. Autentica Y autoriza dentro de la acción — nunca confíes en la compuerta del Client Component que la llama.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("No autorizado");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Prohibido");
}
await db.user.delete({ where: { id: targetId } });
}
React.cache() para deduplicación por petición
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache deduplica dentro de una sola petición. Llamar a getUser("1") desde tres Server Components en el mismo render = una consulta a la DB.
Caché LRU para datos entre peticiones
Para datos que NO cambian por petición (configuración, tablas de búsqueda), almacena en caché fuera de React con un caché LRU o unstable_cache.
Evitar serialización duplicada en props RSC
Cuando un Server Component renderiza los mismos datos en múltiples Client Components, los datos se serializan una vez por consumidor. Eleva el Client Component y pasa children.
Elevar E/S estática al ámbito del módulo
// CORRECTO — se ejecuta una vez al cargar el módulo
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}
Sin estado mutable a nivel de módulo en RSC/SSR
El estado del módulo en el servidor se comparte entre todas las peticiones — una condición de carrera entre usuarios. Usa almacenamiento con ámbito de petición (headers(), cookies(), contexto asíncrono) en su lugar.
Minimizar datos pasados a Client Components
Solo serializa lo que el Cliente necesita. Elimina campos, paginación, proyecta columnas en la capa de DB.
Paralelizar búsquedas anidadas con Promise.all por elemento
const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);
Usar after() para trabajo no bloqueante
Next.js 15 after() ejecuta trabajo después de que se envía la respuesta — registro, calentamiento de caché, analíticas.
import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}
4. Obtención de datos en el cliente (MEDIO-ALTO)
SWR / TanStack Query para deduplicación
Múltiples componentes llamando a useUser(id) deben compartir una solicitud de red y una entrada de caché. Usa SWR o TanStack Query — nunca implementes tu propio useEffect + fetch para datos compartidos.
Deduplicar listeners de eventos globales
// INCORRECTO — cada componente añade el suyo
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// CORRECTO — un solo listener compartido mediante un hook + sujeto global
const useScroll = createScrollHook(); // sujeto singleton internamente
Listeners pasivos para scroll
window.addEventListener("scroll", handler, { passive: true });
Mejora la fluidez del scroll; el listener no puede hacer preventDefault().
localStorage: versión + minimizar
- Siempre almacena un campo
version; increméntalo en cambios de esquema y migra o descarta datos antiguos - Mantén los payloads pequeños —
localStoragees síncrono y bloquea el hilo principal
5. Optimización de re-renderizados (MEDIO)
No te suscribas a estado usado solo en callbacks
// INCORRECTO — re-renderiza cada vez que count cambia
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// CORRECTO — leer una vez en la llamada
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Extraer trabajo costoso en componentes memoizados
// CORRECTO — el hijo re-renderiza solo cuando `items` cambia
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
Elevar props no primitivas por defecto
// INCORRECTO — nuevo array en cada render rompe memo
<List items={items ?? []} />
// CORRECTO
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
Dependencias primitivas en efectos
// INCORRECTO — nueva identidad de objeto en cada render
useEffect(() => {}, [{ id, name }]);
// CORRECTO — primitivos
useEffect(() => {}, [id, name]);
Suscribirse a booleanos derivados, no a valores crudos
// INCORRECTO — re-renderiza por cualquier cambio en cart
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRECTO — re-renderiza solo cuando cambia el estado vacío/no vacío
const hasItems = useStore((s) => s.cart.length > 0);
Derivar durante el render, nunca mediante useEffect
// INCORRECTO
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// CORRECTO
const full = `${first} ${last}`;
setState funcional para callbacks estables
// CORRECTO
const increment = useCallback(() => setCount((c) => c + 1), []);
Inicializador de estado perezoso para valores costosos
const [tree] = useState(() => parseTree(largeInput));
Evitar memo para primitivos simples
useMemo(() => x + 1, [x]) es sobrecarga. Memo vale la pena en identidad de objetos y cómputo costoso.
Dividir hooks con dependencias independientes
// INCORRECTO — ambos selectores se re-ejecutan si alguna fuente cambia
const { a, b } = useSomething(source1, source2);
// CORRECTO
const a = useA(source1);
const b = useB(source2);
Mover lógica de interacción a event handlers
Los event handlers se ejecutan solo en la acción del usuario — useEffect se re-ejecuta cada vez que cambian las dependencias.
startTransition para actualizaciones no urgentes
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
useDeferredValue para renders costosos
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
useRef para valores frecuentes transitorios
Para valores que cambian a menudo pero no deben provocar re-render (timestamps, última tecla, acumuladores).
No definir componentes dentro de componentes
// INCORRECTO — Inner es un nuevo componente en cada render de Outer
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Cada render crea un nuevo tipo Inner, derrotando la reconciliación y desmontando hijos.
6. Rendimiento de renderizado (MEDIO)
Animar el contenedor, no el SVG
Transformar un <div> contenedor alrededor de un SVG está acelerado por GPU; transformar el SVG en sí mismo provoca pintado.
content-visibility: auto para listas largas
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
El navegador salta el renderizado fuera de pantalla — gran mejora para listas con cientos de filas.
Elevar JSX estático
const STATIC_HEADER = <h1>Título</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG: reducir precisión de coordenadas
d="M10.123456,20.654321" → d="M10.12,20.65". Cada dígito cuesta bytes; la diferencia visual es sub-píxel.
Sin parpadeo de hidratación mediante script en línea
Para valores necesarios antes de la hidratación (tema, locale), inserta un <script> que establezca document.documentElement.dataset.* antes de que React monte.
Suprimir discrepancias de hidratación esperadas de forma limitada
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Usar SOLO para nodos hoja con divergencia conocida — nunca en un árbol que contenga otros hijos.
<Activity> para mostrar/ocultar en lugar de montar/desmontar
React 19 <Activity mode="visible|hidden"> mantiene el estado del árbol y los efectos montados pero oculta — más barato que desmontar/remontar para pestañas y acordeones.
Ternario sobre && para renderizado condicional
// INCORRECTO — `0` se renderiza como nodo de texto
{count && <Badge>{count}</Badge>}
// CORRECTO
{count > 0 ? <Badge>{count}</Badge> : null}
useTransition para estados de carga
Combina startTransition con la acción; React muestra la UI anterior como isPending mientras se calcula el siguiente estado.
Pistas de recursos de React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")
...
[truncado para traducción]