Rendimiento en React
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 un árbol de decisiones para la revisión y refactorización activa de código.
Cuándo activarlo
- Al escribir o revisar código React/Next.js para rendimiento
- Al diagnosticar cargas lentas de página, interacciones lentas o alto uso de CPU en el cliente
- Al auditar el tamaño del bundle o regresiones de Core Web Vitals de Lighthouse
- Al eliminar 'waterfalls' (cascadas) en Server Components / rutas API
- Al reducir re-renderizados en el cliente
- Al optimizar listas largas, animaciones o hidratación
- Al auditar elecciones 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 Waterfalls | async- | Cada vez que await es seguido por 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 nativo 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 efecto-evento, referencias estables |
1. Eliminación de Waterfalls (CRÍTICO)
"Los 'waterfalls' (cascadas) son el asesino número uno del rendimiento" — cada
awaitsecuencial agrega latencia de red completa.
Condiciones baratas antes de await
Verifica condiciones síncronas (props, env, flags fijas en código) 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 — cortocircuito en 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);
}
Diferir awaits hasta que se usen
Mueve await a la rama que lo utiliza.
// 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 — iniciar temprano, esperar tarde
// CORRECTO — lanza todas las promesas, espera 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 límites de <Suspense> cerca de los datos para que la página pinte lo que pueda mientras los sub-árboles más lentos se transmiten. La compensación: cambio de diseño ((layout shift)) cuando llega el contenido — reserva espacio (esqueleto o min-height).
Server Components: paralelizar 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 de barril index.ts obligan al empaquetador ((bundler)) a recorrer todo el grafo de módulos incluso cuando el tree-shaking ((eliminación de código muerto)) elimina la mayor parte. Las importaciones directas ahorran 200-800 ms 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 (usa esta configuración); las importaciones directas manuales aún son necesarias para librerías no listadas.
Rutas analizables estáticamente
// INCORRECTO — anula el análisis del empaquetador/rastreo
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 solo es para cliente
});
Diferir scripts de terceros
Carga scripts de analítica, logging y 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");
// ...
}
Precargar al pasar el ratón/enfocar
Dispara <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
Cada función "use server" es un endpoint público. Autentica Y autoriza dentro de la acción — nunca confíes en la protección del Componente Cliente que la llama.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: targetId } });
}
React.cache() para deduplicación por solicitud
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache deduplica dentro de una misma solicitud. Llamar a getUser("1") desde tres Server Components en el mismo renderizado = una consulta a la base de datos.
Caché LRU para datos entre solicitudes
Para datos que NO cambian por solicitud (configuración, tablas de búsqueda), almacena en caché fuera de React con una caché LRU o unstable_cache.
Evitar serialización duplicada en props de 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 solicitudes — una condición de carrera entre usuarios. Usa almacenamiento con ámbito de solicitud (headers(), cookies(), contexto asíncrono) en su lugar.
Minimizar datos pasados a Client Components
Serializa solo lo que el Cliente necesita. Elimina campos, pagina, proyecta columnas a nivel de base de datos.
Paralelizar fetchs anidados 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
after() de Next.js 15 ejecuta trabajo después de que se envía la respuesta — logging, calentamiento de caché, analítica.
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 que llaman a useUser(id) deberían 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 único listener compartido mediante un hook + sujeto global
const useScroll = createScrollHook(); // sujeto singleton bajo el capó
Listeners pasivos para scroll
window.addEventListener("scroll", handler, { passive: true });
Mejora la suavidad del desplazamiento; el listener no puede llamar a preventDefault().
localStorage: versión + minimizar
- Siempre almacena un campo
version; aumenta 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 — lee una vez al llamar
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Extrae 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)} />;
});
Eleva props no primitivas por defecto
// INCORRECTO — nuevo array cada render rompe memo
<List items={items ?? []} />
// CORRECTO
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
Dependencias primitivas en efectos
// INCORRECTO — nueva identidad de objeto cada render
useEffect(() => {}, [{ id, name }]);
// CORRECTO — primitivas
useEffect(() => {}, [id, name]);
Suscríbete a booleanos derivados, no a valores brutos
// INCORRECTO — re-renderiza por cualquier cambio en el carrito
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRECTO — re-renderiza solo cuando cambia el estado de vacío
const hasItems = useStore((s) => s.cart.length > 0);
Deriva durante el renderizado, 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));
Evita memo para primitivas simples
useMemo(() => x + 1, [x]) es sobrecarga. Memo vale la pena para identidad de objetos y cómputo costoso.
Divide hooks con dependencias independientes
// INCORRECTO — ambos selectores se re-ejecutan si cualquiera de las fuentes cambia
const { a, b } = useSomething(source1, source2);
// CORRECTO
const a = useA(source1);
const b = useB(source2);
Mueve la lógica de interacción a manejadores de eventos
Los manejadores de eventos se ejecutan solo con la acción del usuario — useEffect se re-ejecuta cada vez que las dependencias cambian.
startTransition para actualizaciones no urgentes
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
useDeferredValue para renderizados costosos
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
useRef para valores transitorios frecuentes
Para valores que cambian a menudo pero no deben disparar un re-renderizado (timestamps, última tecla, acumuladores).
No definas componentes dentro de componentes
// INCORRECTO — Inner es un componente nuevo en cada render de Outer
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Cada render crea un nuevo tipo Inner, anulando la reconciliación y desmontando hijos.
6. Rendimiento de Renderizado (MEDIO)
Anima el contenedor, no el SVG
Transformar un <div> contenedor alrededor de un SVG está acelerado por GPU; transformar el SVG mismo dispara pintado.
content-visibility: auto para listas largas
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
El navegador omite el renderizado fuera de pantalla — gran beneficio para listas con cientos de filas.
Eleva JSX estático
const STATIC_HEADER = <h1>Title</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG: reduce la precisión de coordenadas
d="M10.123456,20.654321" → d="M10.12,20.65". Cada dígito cuesta bytes; la diferencia visual es sub-pixel.
Sin parpadeo de hidratación mediante script inline
Para valores necesarios antes de la hidratación (tema, locale), incluye un <script> inline que establezca document.documentElement.dataset.* antes de que React monte.
Suprime discrepancias de hidratación esperadas de forma estrecha
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Usa SOLO en nodos hoja conocidos por divergir — nunca en un árbol que contenga otros hijos.
<Activity> para mostrar/ocultar en lugar de montar/desmontar
<Activity mode="visible|hidden"> de React 19 mantiene el estado del árbol y los efectos montados pero los oculta — más barato que desmontar/volver a montar 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 el siguiente estado se computa.
Pistas de recursos de React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")