React DevTools Profiler — Mide el rendimiento del renderizado y encuentra cuellos de botella
Receta
// Paso 1: Instala la extensión React DevTools del navegador
// Chrome: chrome://extensions -> busca "React Developer Tools"
// Firefox: Add-ons -> busca "React Developer Tools"
// Paso 2: Abre DevTools -> pestaña Profiler -> Haz clic en Record -> Interactúa -> Detén
// Paso 3: Usa la API React Profiler para medición en producción
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id, // "Dashboard"
phase, // "mount" or "update"
actualDuration, // Tiempo dedicado al renderizado (ms)
baseDuration, // Tiempo estimado sin memoización (ms)
startTime,
commitTime
) => {
if (actualDuration > 16) {
// Más largo que un frame a 60fps
console.warn(`Renderizado lento: ${id} tomó ${actualDuration.toFixed(1)}ms`);
}
};
function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}Cuándo usarlo: Antes de cualquier trabajo de optimización. Perfila primero, luego arregla. Sin perfilado, podrías optimizar componentes que ya son rápidos mientras pierdes el verdadero cuello de botella.
Ejemplo Funcional
// ---- Perfilando un dashboard lento y arreglando el cuello de botella ----
// ANTES: Dashboard se renderiza en ~180ms — Profiler muestra que OrderTable es el culpable
import { Profiler, useState, useMemo, memo, useCallback } from "react";
import type { ProfilerOnRenderCallback } from "react";
interface Order {
id: string;
customer: string;
total: number;
status: "pending" | "shipped" | "delivered";
date: string;
}
// Logger de rendimiento — envía renderizados lentos a tu analítica
const perfLog: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration) => {
// Registra todos los renderizados durante el desarrollo
console.table({
component: id,
phase,
actual: `${actualDuration.toFixed(1)}ms`,
base: `${baseDuration.toFixed(1)}ms`,
saved: `${(baseDuration - actualDuration).toFixed(1)}ms`,
});
// En producción, solo reporta renderizados lentos
if (process.env.NODE_ENV === "production" && actualDuration > 50) {
fetch("/api/perf", {
method: "POST",
body: JSON.stringify({ id, phase, actualDuration, baseDuration }),
});
}
};
// Dashboard perfilado — envuelve cada sección independientemente
function Dashboard() {
const [orders, setOrders] = useState<Order[]>(generateOrders(1000));
const [statusFilter, setStatusFilter] = useState<string>("all");
const [refreshCount, setRefreshCount] = useState(0);
return (
<div className="grid grid-cols-3 gap-4">
<Profiler id="StatsPanel" onRender={perfLog}>
<StatsPanel orders={orders} />
</Profiler>
<Profiler id="OrderTable" onRender={perfLog}>
<OrderTable
orders={orders}
statusFilter={statusFilter}
onFilterChange={setStatusFilter}
/>
</Profiler>
<Profiler id="ActivityFeed" onRender={perfLog}>
<ActivityFeed refreshCount={refreshCount} />
</Profiler>
<button onClick={() => setRefreshCount((c) => c + 1)}>Actualizar</button>
</div>
);
}
// ANTES: StatsPanel — recomputa en cada renderizado (~12ms)
function StatsPanel({ orders }: { orders: Order[] }) {
const revenue = orders.reduce((sum, o) => sum + o.total, 0);
const pending = orders.filter((o) => o.status === "pending").length;
const shipped = orders.filter((o) => o.status === "shipped").length;
return (
<div className="grid grid-cols-3 gap-2">
<div>Ingresos: ${revenue.toLocaleString()}</div>
<div>Pendiente: {pending}</div>
<div>Enviado: {shipped}</div>
</div>
);
}
// DESPUÉS: StatsPanel — computación memoizada (~0.1ms en re-renderizado)
const OptimizedStatsPanel = memo(function StatsPanel({ orders }: { orders: Order[] }) {
const stats = useMemo(() => ({
revenue: orders.reduce((sum, o) => sum + o.total, 0),
pending: orders.filter((o) => o.status === "pending").length,
shipped: orders.filter((o) => o.status === "shipped").length,
}), [orders]);
return (
<div className="grid grid-cols-3 gap-2">
<div>Ingresos: ${stats.revenue.toLocaleString()}</div>
<div>Pendiente: {stats.pending}</div>
<div>Enviado: {stats.shipped}</div>
</div>
);
});
// ANTES: OrderTable — filtra y renderiza 1000 filas en cada renderizado padre (~160ms)
function OrderTable({
orders,
statusFilter,
onFilterChange,
}: {
orders: Order[];
statusFilter: string;
onFilterChange: (status: string) => void;
}) {
const filtered = orders.filter(
(o) => statusFilter === "all" || o.status === statusFilter
);
return (
<table>
<thead>
<tr>
<th>Cliente</th>
<th>Total</th>
<th>
<select value={statusFilter} onChange={(e) => onFilterChange(e.target.value)}>
<option value="all">Todos</option>
<option value="pending">Pendiente</option>
<option value="shipped">Enviado</option>
<option value="delivered">Entregado</option>
</select>
</th>
</tr>
</thead>
<tbody>
{filtered.map((order) => (
<tr key={order.id}>
<td>{order.customer}</td>
<td>${order.total}</td>
<td>{order.status}</td>
</tr>
))}
</tbody>
</table>
);
}
// DESPUÉS: OrderTable — filtrado memoizado y virtualizado (~3ms en re-renderizado)
const OptimizedOrderTable = memo(function OrderTable({
orders,
statusFilter,
onFilterChange,
}: {
orders: Order[];
statusFilter: string;
onFilterChange: (status: string) => void;
}) {
const filtered = useMemo(
() => orders.filter((o) => statusFilter === "all" || o.status === statusFilter),
[orders, statusFilter]
);
// Muestra solo las primeras 50 filas + virtualiza el resto
const visible = filtered.slice(0, 50);
return (
<table>
<thead>
<tr>
<th>Cliente</th>
<th>Total</th>
<th>
<select value={statusFilter} onChange={(e) => onFilterChange(e.target.value)}>
<option value="all">Todos</option>
<option value="pending">Pendiente</option>
<option value="shipped">Enviado</option>
<option value="delivered">Entregado</option>
</select>
</th>
</tr>
</thead>
<tbody>
{visible.map((order) => (
<tr key={order.id}>
<td>{order.customer}</td>
<td>${order.total}</td>
<td>{order.status}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={3}>
Mostrando {visible.length} de {filtered.length} pedidos
</td>
</tr>
</tfoot>
</table>
);
});Lo que esto demuestra:
- Envolver cada sección del dashboard en su propio
Profileridentifica OrderTable como el cuello de botella (160ms vs 12ms para StatsPanel) - El callback
onRenderregistra duración real vs duración base, mostrando cuánto ahorra la memoización - Después de la optimización: el renderizado del Dashboard baja de ~180ms a menos de 15ms (reducción del 92%)
- El perfilado en producción envía renderizados lentos a un endpoint de analítica para monitoreo
Inmersión Profunda
Cómo Funciona
- Flame Chart — Muestra un árbol de componentes con duración de renderizado. Las barras más anchas significan renderizados más lentos. Las barras grises indican componentes que no se renderizaron (saltados por memo). Busca las barras más anchas para encontrar cuellos de botella.
- Ranked Chart — Enumera componentes ordenados por tiempo de renderizado, los más costosos primero. Este es el camino más rápido para identificar qué componentes optimizar.
- Conteo de renderizados de componentes — Haz clic en cualquier componente del Profiler para ver cuántas veces se renderizó durante la grabación, y la duración de cada renderizado. Conteos altos de renderizados con duración consistente sugieren re-renderizados innecesarios.
- "¿Por qué se renderizó esto?" — Activa en configuración de React DevTools bajo Profiler. Cuando está activado, hacer clic en un componente en el perfil muestra qué props o state cambiaron para desencadenar el renderizado.
- Resaltar actualizaciones — Activa "Resaltar actualizaciones cuando los componentes se renderizan" en configuración de la pestaña Components. Los componentes parpadean con un borde de color en cada renderizado. El color indica frecuencia de renderizado: azul (raro) a rojo (frecuente).
- Selector de commit — El gráfico de barras en la parte superior del Profiler representa commits individuales. Las barras más altas son commits más lentos. Haz clic en cada una para ver qué se renderizó en ese commit.
Variaciones
Why Did You Render (biblioteca de terceros):
// npm install @welldone-software/why-did-you-render
// wdyr.ts — importa antes que React
import React from "react";
if (process.env.NODE_ENV === "development") {
const whyDidYouRender = require("@welldone-software/why-did-you-render");
whyDidYouRender(React, {
trackAllPureComponents: true,
logOnDifferentValues: true,
});
}
// Marca componentes específicos para seguimiento
function ExpensiveList({ items }: { items: Item[] }) {
return <ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}
ExpensiveList.whyDidYouRender = true;
// Salida de consola:
// ExpensiveList: Se re-renderizó por cambios en props:
// items: [{id: 1}] !== [{id: 1}] (referencia diferente, contenido igual)Marcas de rendimiento personalizadas para producción:
function useRenderPerfMark(componentName: string) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
performance.mark(`${componentName}-render-${renderCount.current}`);
if (renderCount.current > 1) {
performance.measure(
`${componentName}-render`,
`${componentName}-render-${renderCount.current - 1}`,
`${componentName}-render-${renderCount.current}`
);
}
});
}
function Dashboard() {
useRenderPerfMark("Dashboard");
return <div>{/* ... */}</div>;
}
// Ver en Chrome DevTools -> pestaña Performance -> carril TimingsNotas de TypeScript
ProfilerOnRenderCallbackes el tipo para el proponRender. Importe desde"react".- El componente
Profileraceptaid: string,onRender: ProfilerOnRenderCallback, ychildren: ReactNode. - El parámetro
phaseestá escrito como"mount" | "update" | "nested-update".
Trampas
-
Perfilado en modo desarrollo — React development mode añade sobrecarga significativa (trazas de pila de componentes, efectos invocados dos veces). Los tiempos de renderizado en dev son 2-5x más lentos que en producción. Solución: Usa la compilación
react-dom/profilingpara perfilado en producción. Añade"profiling": truea la configuración de Next.js. -
DevTools ralentizando la app — La extensión React DevTools en sí añade sobrecarga, especialmente con la pestaña Components abierta. Solución: Cierra la pestaña Components mientras perfilas. Solo usa la pestaña Profiler durante la grabación.
-
API Profiler eliminada en producción — Por defecto, el componente
Profileres una no-op en compilaciones de producción. Solución: Usa la compilación de perfilado:import ReactDOM from "react-dom/profiling"y estableceresolve.aliasen tu configuración del bundler. -
Medir renderizados individuales en lugar de interacciones — Una única grabación de perfil puede no capturar el patrón de interacción real del usuario. Solución: Registra flujos de usuario completos (carga de página, búsqueda, navegación) y mira la línea de tiempo completa.
-
Ignorar la duración base — La
baseDurationen el callback del Profiler te dice cuánto tiempo tomaría el renderizado sin ninguna memoización. SiactualDurationestá cerca debaseDuration, la memoización no está ayudando. Solución: Compara ambos valores. Si la diferencia es pequeña, considera eliminar la memoización para reducir complejidad.
Alternativas
| Enfoque | Compensación |
|---|---|
| React DevTools Profiler | Visual, interactivo; solo desarrollo sin compilación de perfilado |
API Profiler | Programático, funciona en producción; requiere compilación de perfilado |
| Why Did You Render | Muestra cambios exactos de props causando re-renderizados; solo dev, añade sobrecarga |
| Pestaña Chrome Performance | Línea de tiempo completa del navegador incluyendo layout y paint; nivel más bajo |
performance.mark / performance.measure | API nativa del navegador; requiere instrumentación manual |
| Lighthouse | Auditoría automatizada; se enfoca en carga de página, no interacciones en runtime |
Preguntas Frecuentes
¿Cuál es el primer paso antes de optimizar el rendimiento de cualquier componente React?
Perfila primero. Usa React DevTools Profiler para grabar interacciones e identificar qué componentes son realmente lentos. Sin perfilado, podrías optimizar componentes que ya son rápidos mientras pierdes el verdadero cuello de botella.
¿Cómo usas la API React Profiler para registrar renderizados lentos?
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id, phase, actualDuration, baseDuration
) => {
if (actualDuration > 16) {
console.warn(`Renderizado lento: ${id} tomó ${actualDuration.toFixed(1)}ms`);
}
};
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>¿Cuál es la diferencia entre actualDuration y baseDuration en el callback del Profiler?
- actualDuration: tiempo dedicado al renderizado con memoización activa
- baseDuration: tiempo estimado sin ninguna memoización
Si ambos valores son cercanos, la memoización no está ayudando y puede ser removida para reducir complejidad.
¿Cómo identificas el componente más costoso usando el Profiler?
Usa la vista Ranked Chart en el DevTools Profiler. Enumera componentes ordenados por tiempo de renderizado, los más costosos primero. Este es el camino más rápido para encontrar qué componentes optimizar.
¿Qué muestra "¿Por qué se renderizó esto?" en React DevTools?
Actívalo en la configuración de React DevTools bajo Profiler. Cuando está activado, hacer clic en un componente en el perfil muestra qué props o state específicos cambiaron para desencadenar el renderizado. Esto identifica si un re-renderizado es necesario o desperdiciado.
Trampa: ¿Por qué los tiempos de renderizado son 2-5x más lentos en modo desarrollo?
React development mode añade sobrecarga significativa incluyendo trazas de pila de componentes y efectos invocados dos veces (Strict Mode). Siempre usa la compilación de perfilado (react-dom/profiling) para mediciones precisas en producción.
Trampa: ¿Afecta la extensión React DevTools en sí el rendimiento?
Sí. La extensión DevTools añade sobrecarga, especialmente con la pestaña Components abierta. Cierra la pestaña Components mientras perfilas y solo usa la pestaña Profiler durante la grabación para obtener resultados más precisos.
¿Cómo habilitas el perfilado en una compilación de producción de Next.js?
El componente Profiler es una no-op en producción por defecto. Para habilitarlo, usa la compilación de perfilado de React DOM y añade "profiling": true a tu configuración de Next.js.
¿Cuál es el tipo TypeScript correcto para el callback onRender del Profiler?
import { type ProfilerOnRenderCallback } from "react";El parámetro phase está escrito como "mount" | "update" | "nested-update".
¿Cómo complementa la biblioteca Why Did You Render a React DevTools?
Registra los cambios exactos de props causando re-renderizados a la consola, incluyendo casos donde props tienen referencias diferentes pero contenido idéntico. Marca componentes con Component.whyDidYouRender = true para seguimiento.
¿Deberías perfilar renderizados individuales o flujos de usuario completos?
Perfila flujos de usuario completos (carga de página, búsqueda, navegación). Una única grabación de renderizado puede no capturar el patrón de interacción real. Mira la línea de tiempo completa entre múltiples commits para identificar patrones.
¿Cómo puedes usar performance.mark para seguimiento de renderizado personalizado en producción?
useEffect(() => {
renderCount.current += 1;
performance.mark(`${name}-render-${renderCount.current}`);
if (renderCount.current > 1) {
performance.measure(`${name}-render`,
`${name}-render-${renderCount.current - 1}`,
`${name}-render-${renderCount.current}`
);
}
});Ver resultados en la pestaña Chrome DevTools Performance bajo el carril Timings.
Relacionado
- Previniendo Re-renderizados — Arreglando los problemas de re-renderizado que el perfilado revela
- Memoización — Aplicando useMemo, useCallback, y memo donde el perfilado muestra necesidad
- Core Web Vitals — Métricas de rendimiento en producción más allá del renderizado de componentes
- Performance Checklist — Proceso de auditoría sistemática usando herramientas de perfilado