Busca en todas las páginas de la documentación
13 ejemplos para comenzar con React Performance -- 9 básicos y 4 intermedios.
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> 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 <
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: (
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
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>,
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() {
<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
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;
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
<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:
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
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
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
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