Todo este módulo se ha apoyado en una regla que hasta ahora se ha enunciado sin poder aplicarla del todo: no optimices lo que no has medido. Se ha hablado de renders que sobran, de props que cambian de identidad, de cálculos de 40 ms y de paquetes de 271 KB, y cada una de esas afirmaciones era, en realidad, una promesa: «esto se puede comprobar». Esta lección es donde se cobra la promesa. El React DevTools Profiler graba lo que ocurre dentro de React durante una interacción y responde con precisión a las tres preguntas que convierten una sospecha en un diagnóstico: qué componentes se renderizaron, cuánto tardó cada uno y —la más valiosa— por qué. Con esa última respuesta, «creo que la lista se repinta de más» deja de ser una intuición y pasa a ser «TarjetaBicicleta se renderizó 2.000 veces en un commit de 214 ms porque cambió la prop alReservar». La diferencia entre las dos frases es la diferencia entre tocar código a ciegas y arreglar un problema. Al final de la lección recorrerás el caso completo de CicloUrbano —medir, diagnosticar, corregir, volver a medir— y sabrás también lo más difícil de todo: cuándo parar.
Contenido
- Instalar React DevTools y qué aporta cada pestaña
- Por qué se mide en un build de producción
- Anatomía del Profiler: grabar una interacción
- Commits, gráfico de llamas y gráfico ordenado
- Los números: duración del render, tiempo propio y número de renders
- Por qué se repintó esto: las cuatro causas
- Resaltar actualizaciones en el navegador
- Caso de estudio: el buscador de CicloUrbano, antes y después
- La API
<Profiler>en código - Complementos fuera de React
- Cuándo parar de optimizar
- Instalar React DevTools y qué aporta cada pestaña
React DevTools es una extensión oficial disponible para Chrome, Edge y Firefox, y también como aplicación independiente (npx react-devtools) para depurar React Native o páginas servidas fuera del navegador de escritorio.
Una vez instalada, al abrir las herramientas del navegador en una página con React aparecen dos pestañas nuevas:
| Pestaña | Para qué sirve | Cuándo la usas |
|---|---|---|
| ⚛️ Components | Árbol de componentes con sus nombres reales, props, estado, hooks y contextos. Permite editarlos en vivo | Entender la estructura, inspeccionar un valor, comprobar qué contexto llega a un componente |
| ⚛️ Profiler | Grabación de renders con tiempos y causas | Medir el rendimiento: el contenido de esta lección |
El icono de React en la barra del navegador también da información inmediata sin abrir nada: si está en color, la página usa React; y su tono indica si está en modo de desarrollo (rojo) o producción (azul). Ese detalle importa más de lo que parece, y el apartado siguiente explica por qué.
Dos ajustes de la pestaña Components que conviene conocer antes de perfilar:
Highlight updates when components render: dibuja un recuadro alrededor de cada componente que se renderiza. Es el diagnóstico visual más rápido que existe (apartado 7).- Insignia ✨ Memo ✨ junto al nombre de un componente: indica que el React Compiler lo ha optimizado (08-03). Si tu componente crítico no la tiene en un proyecto compilado, es que el compilador lo ha omitido.
- Por qué se mide en un build de producción
Esta es la instrucción más incumplida del módulo, y la que invalida más mediciones.
npm run build # empaqueta con Vite en modo producción
npm run preview # sirve dist/ en http://localhost:4173Qué distorsiona el modo de desarrollo, punto por punto:
| Factor del modo desarrollo | Efecto en las mediciones |
|---|---|
StrictMode ejecuta cada componente dos veces |
Los tiempos de render se duplican. Un componente de 4 ms aparece como 8 |
| Avisos y comprobaciones de React | React valida props, claves y reglas de hooks en cada render: coste que no existe en producción |
| Código sin minificar | Más código que analizar y funciones sin optimizar por el motor de JavaScript |
| Módulos sin empaquetar | Vite sirve cada fichero por separado: cientos de peticiones que en producción son una |
| Mapas de fuentes (source maps) | Memoria y trabajo adicionales del navegador |
| Recarga en caliente (HMR) | Instrumentación permanente en el árbol de componentes |
| La propia instrumentación del Profiler | Añade coste medible, incluso en producción |
Las consecuencias prácticas:
- Los tiempos absolutos del modo desarrollo no sirven. Un commit de 214 ms en desarrollo puede ser de 60 ms en producción.
- Las proporciones sí sirven. Si en desarrollo
TarjetaBicicletaconsume el 80 % del commit, en producción también será el problema. Por eso desarrollar con el Profiler abierto es útil para localizar, aunque las cifras finales haya que tomarlas en un build. - Las conclusiones de tipo «se repintó N veces» sí son válidas en desarrollo, restando el efecto de
StrictMode.
Un detalle técnico importante: el build de producción estándar de React elimina la instrumentación del Profiler. Para perfilar en producción hay que construir con el perfilado activado:
// vite.config.js — solo para builds de medición, no para desplegar
export default defineConfig({
resolve: {
alias: {
'react-dom/client': 'react-dom/profiling'
}
},
build: {
minify: 'terser',
terserOptions: { keep_fnames: true, keep_classnames: true } // nombres legibles
}
});keep_fnames merece un comentario: sin él, la minificación renombra las funciones y el Profiler muestra t, e, n en lugar de TarjetaBicicleta. Con él, el paquete pesa algo más pero el gráfico de llamas es legible. Es una configuración de medición, no de despliegue.
Flujo recomendado: localiza en desarrollo (rápido, con recarga en caliente) y confirma y cuantifica en un build de producción con perfilado. Nunca cierres un asunto con cifras de desarrollo.
- Anatomía del Profiler: grabar una interacción
El ciclo de grabación tiene cinco pasos, y el segundo es el que casi todo el mundo se salta:
- Abre la pestaña Profiler.
- Prepara el estado inicial: navega hasta la pantalla, espera a que los datos hayan cargado, deja la interfaz quieta. Si grabas la carga inicial junto con la interacción, no podrás distinguir una de otra.
- Pulsa el botón grabar (círculo azul).
- Haz una sola cosa: escribe cinco letras en el buscador. Nada más.
- Detén la grabación y analiza.
Antes de grabar, activa estos dos ajustes en el engranaje ⚙ del Profiler:
| Ajuste | Qué hace | Recomendación |
|---|---|---|
| Record why each component rendered | Anota la causa de cada render | ✅ Siempre activado: es la mitad del valor de la herramienta |
| Hide commits below _ ms | Oculta los commits triviales | Útil con 1–2 ms para no perderse en el ruido |
Highlight updates when components render |
Recuadros visuales en la página | Activar para el diagnóstico rápido, desactivar al medir tiempos |
Un principio que ahorra mucho tiempo: una grabación, una interacción. Grabar «abro la aplicación, navego, filtro, escribo y reservo» produce un registro imposible de interpretar. Grabaciones cortas y con una hipótesis concreta.
- Commits, gráfico de llamas y gráfico ordenado
La línea temporal de commits
En la parte superior aparece una serie de barras: cada una es un commit, es decir, una vez que React aplicó cambios al DOM. Recuerda el ciclo de 01-05: render → diferencias → commit. El Profiler mide desde que React empieza a renderizar hasta que termina de aplicar los cambios.
- La altura de cada barra es la duración de ese commit.
- El color va de gris (rápido) a amarillo (lento). El amarillo llama la atención, pero el criterio real son los milisegundos.
- Pulsando una barra se examina ese commit concreto.
Cinco pulsaciones de tecla deberían producir aproximadamente cinco commits. Si ves quince, ya tienes un dato: algo está provocando renders en cascada.
Gráfico de llamas (flamegraph)
Es la vista por defecto y muestra el árbol de componentes de ese commit.
- Anchura = tiempo que ese componente y sus descendientes tardaron en renderizarse.
- Posición vertical = profundidad en el árbol: los hijos debajo de los padres.
- Color: gris = no se renderizó en este commit; de amarillo a verde = se renderizó, con el amarillo indicando más tiempo.
Lo que hay que buscar: barras anchas y amarillas, y sobre todo barras de color donde esperabas gris. Un componente que no debería haberse renderizado y aparece coloreado es exactamente el hallazgo que se busca.
Gráfico ordenado (ranked)
La misma información ordenada de mayor a menor tiempo, sin la estructura del árbol. Es la vista para responder «¿qué me está costando el tiempo?» en dos segundos. El gráfico de llamas responde «¿por qué está pasando?».
flowchart TD
A["Grabar la interaccion"] --> B["Linea temporal:<br/>cuantos commits y de que duracion"]
B --> C{"Hay commits<br/>por encima de 16 ms?"}
C -->|No| Z["No hay problema medible aqui"]
C -->|Si| D["Vista Ranked:<br/>QUE componente consume el tiempo"]
D --> E["Vista Flamegraph:<br/>DONDE esta en el arbol"]
E --> F["Panel lateral:<br/>POR QUE se renderizo"]
F --> G["Diagnostico completo:<br/>componente + coste + causa"]
- Los números: duración del render, tiempo propio y número de renders
Al seleccionar un componente en cualquiera de las dos vistas, el panel lateral muestra sus cifras. Estas son las que hay que saber leer:
| Métrica | Qué mide exactamente | Cómo se interpreta |
|---|---|---|
| Render duration (duración del render) | Tiempo del componente y de todo su subárbol | Alta en un ancestro puede deberse solo a sus hijos: no acuses al padre sin mirar abajo |
| Self time (tiempo propio) | Tiempo solo de ese componente, sin descendientes | Es la métrica que señala al culpable real |
| Renders por commit | Cuántas instancias de ese componente se renderizaron | «2.000» en una lista es la señal clásica |
| Duración total del commit | Todo el trabajo de React en esa actualización | El presupuesto es 16 ms (un fotograma a 60 fps) |
| Ranked position | Puesto en el ranking del commit | Ataca el número 1; el número 12 rara vez importa |
La distinción entre las dos primeras es la que más errores de diagnóstico evita:
PaginaCatalogo render duration: 214 ms self time: 0,8 ms
ListaBicicletas render duration: 212 ms self time: 2,1 ms
TarjetaBicicleta (x2000) self time: 0,1 ms cada una → 209 msPaginaCatalogo parece la culpable con 214 ms, pero su tiempo propio es 0,8 ms: no hace prácticamente nada. El coste está repartido entre 2.000 tarjetas de 0,1 ms cada una. El problema no es que una tarjeta sea cara; es que hay 2.000. Ese matiz decide la solución: aquí no sirve optimizar el interior de TarjetaBicicleta, sirve evitar que se ejecuten (memo) o que existan (paginar, virtualizar).
Y el caso contrario, con el mismo total:
Aquí sí: un solo componente consume 178 ms por sí mismo. Hay un cálculo pesado dentro, y la herramienta es useMemo o un algoritmo mejor.
- Por qué se repintó esto: las cuatro causas
Con «Record why each component rendered» activado, el panel lateral añade la sección «Why did this render?», y ahí está el valor diferencial de la herramienta. Las causas que puede indicar son exactamente las cuatro de 08-01:
| Mensaje del Profiler | Causa | Qué hacer |
|---|---|---|
| «Props changed: (alReservar, alSeleccionar)» | Cambió la identidad de esas props | Estabilizarlas con useCallback/useMemo (08-03), o pasar primitivos |
| «Hooks changed» / «State changed» | Su propio estado cambió | Correcto por definición. Si sobra, el estado está mal colocado: bájalo o súbelo (08-01) |
| «Context changed» | Cambió un contexto que consume | Dividir el contexto y estabilizar su value (07-02 + 08-03). memo no sirve aquí |
| «The parent component rendered» | El padre se renderizó y este no está memorizado | memo (08-02), o aislar con children (08-01) |
La primera es la más valiosa de todas, porque nombra la prop culpable. Sin el Profiler, averiguar cuál de siete props cambió de identidad exige instrumentar el componente a mano (como en 08-02). Con él, es una línea de texto.
Y la lectura estratégica de la tabla, que es la que convierte el módulo entero en un procedimiento:
flowchart TD
A["Why did this render?"] --> B{"Que dice?"}
B -->|"Parent rendered"| C{"El componente<br/>es caro o repetido?"}
C -->|Si| D["memo 08-02"]
C -->|No| E["Dejalo: no es el problema"]
B -->|"Props changed"| F["Que prop?<br/>Estabilizarla con useCallback/useMemo 08-03<br/>o pasar primitivos"]
B -->|"State changed"| G{"Deberia tener<br/>ese estado?"}
G -->|No| H["Bajar el estado 08-01"]
G -->|Si| I["Correcto: no toques nada"]
B -->|"Context changed"| J["Dividir contexto 07-02<br/>+ estabilizar value 08-03"]
- Resaltar actualizaciones en el navegador
Antes de grabar nada, hay un diagnóstico de cinco segundos. En la pestaña Components, engranaje ⚙ → General → Highlight updates when components render.
A partir de ese momento, cada componente que se renderiza se rodea de un recuadro de color durante un instante:
| Color del recuadro | Frecuencia de renders |
|---|---|
| Azul claro | Baja |
| Verde | Media |
| Amarillo | Alta |
| Rojo | Muy alta: mira aquí |
Escribe una letra en el BuscadorBicicletas y observa. Si toda la pantalla se enciende —cabecera, pie, resumen de flota, las 2.000 tarjetas—, tienes el problema localizado sin haber grabado nada. Si solo parpadea el campo de texto, no hay nada que investigar por ahí.
Es la herramienta ideal para tres cosas: una revisión rápida antes de una sesión de perfilado seria, comprobar de un vistazo si un memo recién puesto ha surtido efecto, y detectar renders continuos causados por un efecto mal escrito (un componente que parpadea sin que nadie lo toque es un bucle de renders).
Desactívala al medir tiempos: dibujar los recuadros consume trabajo y contamina las cifras.
- Caso de estudio: el buscador de CicloUrbano, antes y después
Este es el recorrido completo, con el catálogo de 2.000 bicicletas, un build de producción con perfilado y la CPU ralentizada 4× para simular un móvil de gama media.
Paso 1: definir la hipótesis y la interacción
Síntoma reportado: «al escribir en el buscador, las letras tardan en aparecer».
Interacción a medir: escribir «electr» (6 pulsaciones) en BuscadorBicicletas.
Presupuesto (08-01): INP < 200 ms; commits < 16 ms.
Paso 2: medir el estado inicial
Grabación con la lista temporal de commits:
| Commit | Duración | Componentes renderizados |
|---|---|---|
| 1 | 218 ms | 2.014 |
| 2 | 226 ms | 2.014 |
| 3 | 214 ms | 2.014 |
| 4 | 231 ms | 2.014 |
| 5 | 219 ms | 2.014 |
| 6 | 224 ms | 2.014 |
Seis commits de ~220 ms. El presupuesto de 16 ms se incumple catorce veces. La sensación de «las letras tardan» está confirmada con números: cada tecla bloquea el hilo principal más de un quinto de segundo.
Vista Ranked del commit 3:
1. TarjetaBicicleta (x2000) 209,4 ms (self total) 2. ListaBicicletas 2,1 ms 3. PaginaCatalogo 0,8 ms 4. ResumenFlota 0,7 ms 5. BuscadorBicicletas 0,3 ms
Vista Flamegraph: el 95 % de la anchura es la fila de TarjetaBicicleta.
Paso 3: localizar la causa
Se selecciona una TarjetaBicicleta cualquiera y se lee el panel lateral:
TarjetaBicicleta (bici-1487)
Render duration: 0,11 ms
Self time: 0,11 ms
Why did this render?
→ Props changed: (alSeleccionar, alReservar)Diagnóstico completo, y nótese que ya no hay ninguna suposición:
TarjetaBicicletano está memorizada (08-02), así que se reejecuta con cada render del padre.- Aunque lo estuviera,
alSeleccionaryalReservarson funciones flecha creadas en línea enListaBicicletas: elmemofallaría igualmente (08-03). - Cada tarjeta cuesta poco (0,11 ms), pero hay 2.000.
- Además, todo el filtrado ocurre de forma síncrona y urgente en la misma actualización que el tecleo.
Paso 4: aplicar las correcciones
Se aplican, y esto es importante, de una en una, midiendo entre cada paso:
4.1 — memo en TarjetaBicicleta (08-02), sacando además el Intl.NumberFormat fuera del componente.
const FORMATO_EURO = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' });
function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar }) { /* … */ }
export default memo(TarjetaBicicleta);Medición: sin cambios (≈220 ms). Y esto es un resultado, no un fracaso: confirma el punto 2 del diagnóstico. Un memo sin props estables no ahorra nada.
4.2 — useCallback en ListaBicicletas (08-03).
const manejarSeleccion = useCallback((id) => navegar(`/bicicletas/${id}`), [navegar]);
const manejarReserva = useCallback((id) => navegar(`/reservas/nueva?bicicleta=${id}`), [navegar]);Medición: los commits bajan a ~34 ms. Y en el panel lateral de una tarjeta cualquiera, el mensaje ha cambiado a lo que se buscaba:
Solo se renderizan las tarjetas que entran o salen del filtro. El 84 % del coste ha desaparecido con dos hooks, pero solo porque el memo del paso anterior estaba puesto: por separado ninguno de los dos servía.
4.3 — useDeferredValue en PaginaCatalogo (08-03), para que el filtrado deje de competir con el tecleo.
const terminoDiferido = useDeferredValue(termino);
const visibles = useMemo(
() => filtrarYOrdenar(bicicletas, estaciones, terminoDiferido, tipo, orden),
[bicicletas, estaciones, terminoDiferido, tipo, orden]
);Medición: ahora hay dos tipos de commit por pulsación —uno urgente de ~2 ms que solo actualiza el campo, y otro diferido e interrumpible de ~30 ms para la lista—, y en el tecleo rápido varios de los diferidos se abandonan sin llegar a completarse. El campo responde al instante.
Paso 5: volver a medir y comparar
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Duración media del commit | 220 ms | 30 ms (diferido) + 2 ms (urgente) | −86 % |
| Componentes por commit | 2.014 | 14 | −99 % |
| Commits por encima de 16 ms | 6 de 6 | 0 urgentes, 2 diferidos | ✅ |
| INP medido | ~240 ms | ~35 ms | −85 % |
| Presupuesto (< 200 ms INP) | ❌ | ✅ | Cumplido |
Paso 6: decidir si parar
El presupuesto se cumple con margen. Quedan ~30 ms en los commits diferidos, y se podrían atacar virtualizando la lista (08-01). Pero: son diferidos, son interrumpibles, no bloquean el tecleo y el usuario ya no percibe ningún retraso. Virtualizar añadiría una dependencia, complicaría la accesibilidad y rompería Ctrl+F, a cambio de una mejora que nadie va a notar.
Decisión: parar aquí. Documentar las cifras en RENDIMIENTO.md y pasar a otra cosa. Esa decisión es tan parte del oficio como las tres correcciones anteriores.
- La API
<Profiler> en código
<Profiler> en códigoLa extensión mide cuando tú estás delante. Para medir en automático —en pruebas de rendimiento, en integración continua o en producción con usuarios reales— React ofrece un componente:
import { Profiler } from 'react';
<Profiler id="catalogo" onRender={registrarMetrica}>
<PaginaCatalogo />
</Profiler>La función onRender recibe seis argumentos:
function registrarMetrica(
id, // 1) el id del <Profiler>: "catalogo"
fase, // 2) "mount" | "update" | "nested-update"
duracionReal, // 3) ms de este render (con memorización aplicada)
duracionBase, // 4) ms estimados SIN ninguna memorización
inicio, // 5) marca temporal en que React empezó
confirmacion // 6) marca temporal en que React confirmó
) {
// …
}La pareja clave es la tercera con la cuarta. duracionBase es lo que costaría renderizar todo el subárbol sin ningún memo ni useMemo; duracionReal es lo que costó de verdad. La diferencia es, literalmente, lo que tu memorización está ahorrando:
// src/utilidades/metricas.js
const UMBRAL_MS = 16;
export function registrarMetrica(id, fase, duracionReal, duracionBase) {
const ahorro = duracionBase - duracionReal;
if (duracionReal > UMBRAL_MS) {
console.warn(
`[rendimiento] ${id} (${fase}): ${duracionReal.toFixed(1)} ms ` +
`(base ${duracionBase.toFixed(1)} ms, ahorro ${ahorro.toFixed(1)} ms)`
);
}
// En producción: enviar a un servicio de métricas, sin bloquear el hilo
if (import.meta.env.PROD && navigator.sendBeacon) {
navigator.sendBeacon(
'/metricas/render',
JSON.stringify({ id, fase, duracionReal, duracionBase, ts: Date.now() })
);
}
}// src/rutas.jsx (fragmento)
import { Profiler } from 'react';
import { registrarMetrica } from './utilidades/metricas.js';
{
index: true,
element: (
<Profiler id="catalogo" onRender={registrarMetrica}>
<PaginaCatalogo />
</Profiler>
)
}Detalles que hay que conocer antes de usarlo:
| Aspecto | Detalle |
|---|---|
| Coste | No es gratis: añade trabajo en cada render del subárbol. Envuelve zonas concretas, no toda la aplicación |
| En producción estándar | onRender no se llama: se necesita el build con perfilado (apartado 2) |
| Anidamiento | Se pueden anidar <Profiler> con id distintos para medir por zonas |
Un id por zona medida |
El id viaja en cada llamada: úsalo para agregar métricas por pantalla |
| No mide el DOM ni la red | Solo el trabajo de React. Para lo demás, apartado 10 |
nested-update |
Un render provocado por un setEstado dentro de un efecto de disposición: señal de un patrón mejorable |
Un uso muy práctico en pruebas automatizadas: registrar duracionReal en un fichero durante una prueba de extremo a extremo y fallar la construcción si supera un umbral. Es la forma de que un presupuesto de rendimiento se defienda solo, sin depender de que alguien se acuerde de medir. Ese tipo de comprobación automática es, precisamente, el terreno del Módulo 9.
- Complementos fuera de React
El Profiler mide el trabajo de React, y el trabajo de React es una parte del total. Estas son las herramientas que cubren el resto, mencionadas para que sepas cuándo cambiar de instrumento:
| Herramienta | Qué mide que el Profiler no | Cuándo usarla |
|---|---|---|
| Pestaña Rendimiento del navegador | Todo el hilo principal: JavaScript, maquetación, pintado, red, tareas largas | Cuando el Profiler dice que React tarda poco y aun así se nota lento |
| Lighthouse | LCP, CLS, TBT, buenas prácticas, accesibilidad, con una nota global | Auditoría periódica y antes de un despliegue importante |
| Pestaña Red | Tamaño y orden de los fragmentos, cascadas de peticiones | Al validar la división de código de 08-04 |
web-vitals (biblioteca) |
LCP, INP y CLS de usuarios reales en producción | La medición que de verdad importa: la de tus usuarios, no la de tu portátil |
| Ralentización de CPU y red | Simulación de dispositivos y conexiones lentas | Siempre que midas: 4× de CPU y «3G lento» |
PerformanceObserver |
Tareas largas, entradas de usuario, marcas propias | Instrumentación a medida en producción |
La regla para elegir: si el Profiler dice que React consume poco tiempo y la aplicación sigue sintiéndose lenta, el problema no es de React. Estará en la red, en las imágenes, en el CSS, en la maquetación o en una biblioteca de terceros, y la pestaña Rendimiento del navegador es donde se ve.
Sobre web-vitals conviene insistir en algo: tu portátil con localhost no es una muestra representativa de nada. Medir en producción con usuarios reales revela distribuciones —el percentil 75 de INP en móviles Android, por ejemplo— que ninguna medición local muestra. No se desarrolla en esta lección, pero es el destino natural de todo lo aprendido aquí.
- Cuándo parar de optimizar
Es la pregunta que cierra el módulo, y la que menos se enseña. Hay cuatro señales claras de parada:
1. El presupuesto se cumple. Si definiste INP < 200 ms y estás en 35 ms, has terminado. Un presupuesto que no detiene el trabajo cuando se cumple no era un presupuesto.
2. La mejora ya no se percibe. Existe un umbral perceptivo: por debajo de ~100 ms una respuesta se siente instantánea. Pasar de 40 ms a 25 ms es un 37 % de mejora en la hoja de cálculo y cero mejora para el usuario.
3. El coste de lectura supera al beneficio. Un componente con cuatro useMemo, tres useCallback y un comparador personalizado, para ahorrar 3 ms, es un componente que el próximo desarrollador —o tú dentro de seis meses— romperá al modificarlo. Ese riesgo tiene un coste real.
4. Hay un problema mayor en otro sitio. Si la aplicación tarda 3 s en el primer pintado, seguir puliendo un commit de 20 ms es dedicar esfuerzo al lugar equivocado. Vuelve al paso 1 del flujo de 08-01 y elige el mayor cuello de botella, no el más entretenido.
Y la señal de que hay que volver atrás: si una optimización no produjo una mejora medible, revierte. Su coste de complejidad y memoria sigue ahí aunque el beneficio no exista. Esa disciplina es lo que impide que un proyecto se llene de memorización decorativa a lo largo de los años.
flowchart TD
A["He aplicado una optimizacion"] --> B["Volver a medir<br/>build de produccion, CPU 4x"]
B --> C{"Mejora<br/>medible?"}
C -->|No| D["REVERTIR<br/>coste sin beneficio"]
C -->|Si| E{"Se cumple<br/>el presupuesto?"}
E -->|No| F["Siguiente cuello de botella<br/>el MAYOR, no el mas comodo"]
E -->|Si| G{"Alguien percibiria<br/>seguir mejorando?"}
G -->|Si| F
G -->|No| H["PARAR<br/>documentar las cifras y seguir"]
F --> A
D --> F
Errores Comunes y Consejos
Medir en modo desarrollo y creerse las cifras. StrictMode duplica los renders y los avisos añaden coste. Localiza en desarrollo; cuantifica en un build de producción con perfilado.
Grabar demasiado. Una grabación con cinco interacciones distintas es ilegible. Una grabación, una hipótesis.
Perfilar sin «Record why each component rendered». Es renunciar a la mitad de la herramienta: sin la causa, tienes tiempos pero no diagnóstico.
Confundir «render duration» con «self time». El primero incluye a los descendientes. Acusar a un padre con 214 ms de duración y 0,8 ms de tiempo propio es empezar a optimizar el sitio equivocado.
Perseguir el número de componentes renderizados. 2.000 renders de 0,01 ms importan menos que uno de 180 ms. La métrica es el tiempo.
Olvidarse de keep_fnames al construir para perfilar. El gráfico de llamas lleno de t, e y n no sirve para nada.
Dejar <Profiler> envolviendo toda la aplicación en producción. Añade coste en cada render. Envuelve zonas concretas y solo mientras lo necesites.
Consejo: guarda las grabaciones. El Profiler permite exportar un perfil a JSON e importarlo después. Guardar el «antes» junto al «después» convierte una mejora en una prueba, y es material excelente para una revisión de código.
Consejo: ralentiza siempre la CPU 4×. Es la diferencia entre optimizar para tu portátil y optimizar para tus usuarios.
Consejo: escribe las cifras en el repositorio. Un RENDIMIENTO.md con la interacción medida, la fecha, el antes y el después convierte el trabajo de este módulo en algo que sobrevive a quien lo hizo.
Consejo: perfila también cuando todo va bien. Una medición periódica detecta regresiones cuando aún son pequeñas. Encontrar que un commit pasó de 12 ms a 40 ms en la última semana es mucho más fácil que investigar por qué la aplicación «va lenta» seis meses después.
Ejercicios
Ejercicio 1. Interpreta esta grabación de PaginaDetalleEstacion de CicloUrbano, tomada en un build de producción al pulsar la pestaña «Incidencias». Di cuál es el problema, qué no es el problema, y qué técnica de qué lección aplicarías.
Commit 1 — 187 ms — 63 componentes Ranked: 1. PestanaIncidencias 171,2 ms (self: 168,9 ms) 2. TarjetaEstacion 6,4 ms (self: 0,4 ms) 3. MigasDePan 3,1 ms (self: 3,1 ms) 4. Cabecera 2,8 ms (self: 0,2 ms) Panel de PestanaIncidencias: Why did this render? → The parent component rendered
Ejercicio 2. Tras aplicar memo a TarjetaEstacion en PaginaEstaciones, la nueva grabación muestra esto. Explica por qué el memo no ha servido, qué información concreta te lo dice y cómo lo arreglarías.
Commit 1 — 78 ms — 34 componentes Panel de TarjetaEstacion (est-02): Render duration: 2,1 ms Self time: 1,9 ms Why did this render? → Props changed: (plazasLibres, alAbrir, estilo)
Ejercicio 3. Escribe una envoltura <Profiler> que registre en sessionStorage el peor commit de cada zona medida (id), guardando duración, fase y momento, y que avise por consola cuando una zona supere los 16 ms dos veces seguidas. Explica dónde la colocarías en CicloUrbano y por qué no en la raíz.
Soluciones
Solución 1. El problema: PestanaIncidencias consume 168,9 ms de tiempo propio. No es un problema de cantidad de componentes —solo hay 63 en todo el commit— sino de un solo componente que hace un trabajo pesado dentro de su render: probablemente ordena, agrupa o formatea las incidencias, y muy posiblemente con un find dentro de un map o con Intl creado por elemento.
Lo que NO es el problema:
- No es
TarjetaEstacion: 6,4 ms de duración pero 0,4 ms propios. Su coste es el de sus hijos. - No es el número de renders: 63 componentes es una cifra perfectamente sana.
- No es «The parent component rendered», aunque sea la causa indicada. Que el padre renderice es normal al cambiar de pestaña; envolver
PestanaIncidenciasenmemono ahorraría nada, porque este render tenía que ocurrir: el usuario acaba de pulsar la pestaña.
Técnica a aplicar: useMemo sobre el cálculo pesado (08-03), midiendo antes con console.time para confirmar que es caro. Y antes que eso, revisar el algoritmo: un índice Map en lugar de búsquedas lineales y un Intl.Collator reutilizado suelen bajar más el coste que la memorización. Si tras eso el primer render sigue pesando, la opción siguiente es que el cálculo no ocurra en el cliente (select de useQuery, o directamente el servidor).
Solución 2. Por qué no ha servido: la línea Props changed: (plazasLibres, alAbrir, estilo) lo dice literalmente. Tres props cambian de identidad en cada render del padre, así que la comparación superficial de memo falla siempre y el componente se ejecuta igual, con la comparación como coste añadido.
Qué información lo dice: la sección «Why did this render?» con la lista de props. Sin ella habría que instrumentar el componente a mano con un comparador de traza (08-02).
Cómo arreglarlo, prop a prop:
alAbrir: función flecha en línea. Se estabiliza conuseCallbacken el padre, con[navegar]o[despachar]como dependencia (08-03).estilo: objeto literal, casi con seguridad unstyle={{ … }}. Lo mejor no es memorizarlo, sino eliminarlo: pasarlo a una clase de CSS Modules, que es la convención del proyecto.plazasLibres: si es un número, la comparación por valor debería acertar. Que aparezca como cambiada significa que realmente cambia —quizá se recalcula con unfilterque devuelve un valor distinto— o que no es un primitivo. Habría que inspeccionar el valor en la pestaña Components. Si el dato cambia de verdad, ese render es correcto y no hay nada que arreglar.
Y la comprobación final, que es lo que cierra el ciclo: volver a grabar. El panel debe pasar a decir Did not render en las tarjetas cuyos datos no han cambiado. Si no lo dice, el arreglo no ha funcionado y hay que volver al diagnóstico.
Solución 3.
// src/utilidades/metricas.js
const UMBRAL_MS = 16;
const CLAVE = 'metricas:peores';
const consecutivos = new Map(); // id → número de commits seguidos por encima del umbral
function leerPeores() {
try {
return JSON.parse(sessionStorage.getItem(CLAVE) ?? '{}');
} catch {
return {}; // sessionStorage puede fallar en modo privado
}
}
export function registrarMetrica(id, fase, duracionReal, duracionBase, inicio) {
// 1) Guardar el peor commit por zona
const peores = leerPeores();
const peorActual = peores[id]?.duracionReal ?? 0;
if (duracionReal > peorActual) {
peores[id] = {
duracionReal: Number(duracionReal.toFixed(2)),
duracionBase: Number(duracionBase.toFixed(2)),
ahorro: Number((duracionBase - duracionReal).toFixed(2)),
fase,
momento: new Date(performance.timeOrigin + inicio).toISOString()
};
try {
sessionStorage.setItem(CLAVE, JSON.stringify(peores));
} catch { /* cuota llena o modo privado: no es motivo para romper la app */ }
}
// 2) Avisar solo tras DOS commits seguidos por encima del umbral
if (duracionReal > UMBRAL_MS) {
const seguidos = (consecutivos.get(id) ?? 0) + 1;
consecutivos.set(id, seguidos);
if (seguidos >= 2) {
console.warn(
`[rendimiento] "${id}" lleva ${seguidos} commits seguidos por encima de ` +
`${UMBRAL_MS} ms (último: ${duracionReal.toFixed(1)} ms, ` +
`base ${duracionBase.toFixed(1)} ms). Perfila esta zona.`
);
}
} else {
consecutivos.set(id, 0); // un commit rápido reinicia la racha
}
}
export function obtenerPeores() {
return leerPeores(); // para volcarlo al final de una prueba E2E
}// src/rutas.jsx (fragmento)
{
index: true,
element: (
<Profiler id="catalogo" onRender={registrarMetrica}>
<PaginaCatalogo />
</Profiler>
)
},
{
path: 'estaciones/:estacionId',
element: (
<Profiler id="detalle-estacion" onRender={registrarMetrica}>
<Perezosa><PaginaDetalleEstacion /></Perezosa>
</Profiler>
)
}Dónde colocarla y por qué no en la raíz:
- Se envuelve por zonas con significado propio —el catálogo, el detalle de estación, el panel de taller—, porque el
ides lo que permite atribuir un problema a una pantalla concreta. Un único<Profiler id="app">daría un número global que no señala nada. - No en la raíz, por tres razones:
onRenderse dispara en cada render de todo el subárbol, y en la raíz eso es todos los renders de la aplicación (coste medible añadido); la duración agregada mezcla trabajo de zonas independientes y pierde toda capacidad de diagnóstico; y el umbral de 16 ms deja de tener sentido cuando lo que se mide es «toda la aplicación» en lugar de una interacción concreta. - El aviso tras dos commits seguidos evita el ruido de los picos aislados —una carga inicial, una revalidación de Query— y señala solo lo que es sostenido, que es lo que el usuario percibe.
Conclusión
El React DevTools Profiler es la herramienta que convierte todo este módulo en un procedimiento verificable. Su valor está en responder tres preguntas con datos: qué se renderizó, cuánto costó y por qué. Sin la tercera, las otras dos solo alimentan sospechas.
El método completo, tal y como lo has aplicado: medir en un build de producción (npm run build + npm run preview, con el alias a react-dom/profiling y keep_fnames para que los nombres sigan siendo legibles), porque el modo de desarrollo duplica los renders con StrictMode, añade avisos, sirve módulos sin empaquetar y arrastra mapas de fuentes; localizar en desarrollo, cuantificar en producción. Grabar una sola interacción por sesión, con «Record why each component rendered» siempre activado. Leer la línea temporal de commits —cada barra es una aplicación de cambios al DOM, con el presupuesto de 16 ms por fotograma—, usar el gráfico ordenado para saber qué cuesta y el gráfico de llamas para saber dónde está en el árbol. Y no confundir nunca «render duration» (el componente y todo su subárbol) con «self time» (solo él): un padre de 214 ms con 0,8 ms propios no es el culpable, y esa distinción decide si la solución es memorizar, virtualizar o cambiar un algoritmo.
La sección «Why did this render?» mapea exactamente sobre las cuatro causas de 08-01, y cada una tiene su respuesta: props changed con el nombre de la prop culpable → estabilizarla o pasar primitivos (08-03); state changed → correcto, salvo que el estado esté mal colocado (08-01); context changed → dividir el contexto y estabilizar su value (07-02 + 08-03), donde memo no sirve de nada; the parent component rendered → memo si el componente es caro o repetido (08-02), o aislarlo con children. A eso se suma «Highlight updates», el diagnóstico de cinco segundos que enciende en rojo lo que se repinta demasiado.
El caso de estudio de CicloUrbano ha recorrido el ciclo entero con cifras reales: seis commits de ~220 ms con 2.014 componentes al escribir seis letras; el diagnóstico exacto —Props changed: (alSeleccionar, alReservar)—; y las tres correcciones aplicadas de una en una, midiendo entre cada paso. El memo solo no cambió nada, y eso fue un resultado valioso, no un fracaso: confirmó que la memorización sin identidades estables es coste puro. Con useCallback los commits bajaron a 34 ms y las tarjetas pasaron a decir Did not render; con useDeferredValue el tecleo dejó de competir con el filtrado. De 220 ms a 30 ms, de 2.014 componentes a 14, un INP de 240 ms a 35 ms. Y la última decisión, tan importante como las anteriores: parar, aunque quedara margen técnico, porque virtualizar habría añadido complejidad y roto la accesibilidad a cambio de una mejora imperceptible. También conoces la API <Profiler> con su onRender y la pareja duracionReal/duracionBase, que mide directamente lo que tu memorización está ahorrando, y sabes que el Profiler no lo ve todo: para maquetación, pintado, red y usuarios reales están la pestaña Rendimiento, Lighthouse y web-vitals.
Con esto se cierra el Módulo 8. CicloUrbano ya no repinta 2.000 tarjetas por cada tecla, no recalcula lo que no ha cambiado, no crea funciones nuevas donde alguien compara identidades, no descarga el panel de taller para enseñar el catálogo y —lo que más vale— no depende de que nadie adivine nada: cada decisión de este módulo se apoya en un número reproducible, y la disciplina de revertir lo que no mejora impide que el proyecto se llene de optimización decorativa. Pero fíjate en lo que ha hecho falta para llegar aquí: se ha cambiado la firma de ListaBicicletas, se ha movido estado de sitio, se ha reescrito el value de dos proveedores de contexto, se han partido las rutas en nueve fragmentos y se ha añadido una capa de carga perezosa con sus fallos de red. Nueve refactorizaciones sobre código que funcionaba, y la única comprobación ha sido abrir el navegador y mirar. Eso no escala: la próxima vez que alguien estabilice una dependencia y se le cuele un valor congelado, o que un memo deje de actualizar un campo, nadie se enterará hasta que lo cuente un usuario. El Módulo 9: Pruebas en React ataca exactamente ese vacío: por qué una aplicación sin pruebas no se puede refactorizar con confianza, qué merece la pena probar y qué no, y cómo escribir comprobaciones que fallen cuando el comportamiento cambie —no cuando cambie la implementación—. La próxima lección es Introducción a las Pruebas.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
