wandres.dev
RENDIMIENTO · evitar re-renders

Medir: el profiler, contar renders y no optimizar a ciegas

Las cuatro lecciones anteriores dan herramientas para hacer una aplicación más rápida; esta enseña a saber si hacía falta y si funcionó. Recorre el profiler de React —la grabación, el mapa de llamas, la vista por coste acumulado y el motivo de cada render—, las técnicas de conteo con un contador de renders y el componente Profiler para obtener números reproducibles, y los sesgos que invalidan una medición: el modo desarrollo, la duplicación de StrictMode, los datos de juguete y la máquina del programador. Termina con el argumento de por qué cada optimización es deuda hasta que una medición la justifica y una segunda medición demuestra que sirvió.

⏱ 17 min

Las cuatro lecciones anteriores construyeron un arsenal: granularidad, comparadores, memoización, cachés con dueño. Esta última introduce lo único que convierte ese arsenal en ingeniería en vez de superstición, que es la medición. El rendimiento es el área del oficio donde la intuición falla con más frecuencia y más confianza, porque el modelo mental que tenemos del coste de un programa se formó leyendo código, y el coste real lo determinan la máquina virtual, el motor de reconciliación, la caché de la CPU y el navegador. La consecuencia es incómoda pero liberadora: no sabes qué es lento en tu aplicación, y cualquier optimización que hagas sin una medición previa es una apuesta con dinero de otro. Aprender a medir no es un apéndice de este nivel, es lo que decide si todo lo anterior te sirve o solo te da vocabulario para justificar cambios que nadie ha comprobado.

🎯 Al terminar esta lección sabrás
  • Usar el profiler de React para saber qué componentes se renderizan, cuánto cuestan y por qué.
  • Obtener números reproducibles contando renders con un contador propio y con el componente Profiler.
  • Reconocer los cuatro sesgos que invalidan una medición y neutralizarlos antes de concluir nada.
  • Justificar cada optimización con una medición previa y validarla con una posterior.

El profiler: qué renderiza, cuánto cuesta y por qué

La extensión de herramientas de desarrollo de React trae dos instrumentos que responden preguntas distintas y complementarias. El primero es la señalización visual de actualizaciones: al activarla, cada componente que se renderiza parpadea con un borde de color en la propia pantalla. No da números, pero responde en segundos a la pregunta más importante de todas —qué se está repintando cuando toco esto— y suele bastar para descubrir que media interfaz reacciona a un despacho que solo debería afectar a una esquina.

El segundo instrumento es la grabación de sesiones. Pulsas grabar, ejecutas la interacción sospechosa, detienes y obtienes una serie de confirmaciones, cada una con su duración y su árbol. El mapa de llamas muestra la jerarquía de ese commit con el ancho proporcional al tiempo, distinguiendo lo que se renderizó de lo que se saltó. La vista ordenada por coste acumulado responde a la pregunta complementaria: qué componente sumó más milisegundos a lo largo de toda la grabación, que casi nunca es el más caro por render sino el que se renderizó más veces.

🎬

Grabar una interacción, no la app

Una grabación útil es corta y quirúrgica: pulsa grabar, haz una sola cosa, detén. Grabar dos minutos de uso produce un volumen de commits en el que ya no se distingue la señal.

🔥

Mapa de llamas por commit

Responde qué se renderizó en este cambio concreto y cuánto costó cada nodo. Sirve para encontrar el render que no debería haber ocurrido.

📊

Coste acumulado

Responde quién consume más tiempo en total. El culpable suele ser un componente barato renderizado cien veces, no uno caro renderizado una.

El motivo del render

Con la opción de registrar por qué renderizó cada componente, el panel indica si fue por props, por estado, por contexto o por su padre. Es el dato que convierte un síntoma en un diagnóstico.

El motivo del render es el dato más valioso para lo que estudia este nivel, porque distingue las dos familias de causa. Si el panel dice que el componente renderizó porque cambió su estado interno enganchado al store, tienes un problema de selector y las lecciones dos, tres y cuatro te dan la respuesta. Si dice que renderizó porque lo hizo su padre, el selector es inocente y el problema está en la estructura del árbol o en la ausencia de memoización de componentes. Optimizar selectores cuando la causa era el padre es el desperdicio de esfuerzo más habitual del oficio.

Contar renders: el número que no miente

El profiler es excelente para explorar y mediocre para comparar. Sus duraciones fluctúan entre ejecuciones y las diferencias pequeñas se pierden en el ruido. Para saber si un cambio mejoró algo hace falta una métrica discreta, reproducible y sin unidades de tiempo: el número de renders. Un contador es tosco, pero tiene la virtud de que veinte renders son inequívocamente más que tres y ninguna variabilidad de máquina altera esa comparación.

// Contador crudo: sobrevive a StrictMode si lo interpretas con cuidado.
function useContarRenders(nombre: string) {
  const n = useRef(0)
  n.current += 1
  console.log(`${nombre} render numero ${n.current}`)
}

// Medicion estructurada: el componente Profiler entrega tiempos por commit.
function alRenderizar(id, fase, duracionReal, duracionBase) {
  registrar({ id, fase, duracionReal, duracionBase })
}

Contar tiene además una ventaja epistemológica sobre cronometrar: los renders son un efecto directo de las decisiones que tomas en los selectores, mientras que los milisegundos son un agregado de tu código, del motor, del recolector y de lo que el sistema operativo estuviera haciendo en ese instante. Si tu hipótesis es que un selector inestable estaba renderizando de más, el número que la confirma o la refuta es el conteo, no el reloj. Deja el reloj para la pregunta final, que es si el usuario lo nota.

El componente Profiler merece más uso del que recibe, porque permite instrumentar una parte concreta del árbol en cualquier entorno, sin extensión y sin interacción manual. Su devolución de llamada recibe el identificador, la fase —montaje o actualización—, la duración real del subárbol y la duración base, que es lo que habría costado sin ninguna memoización. Comparar duración real contra duración base es la forma más directa de saber si tus memoizaciones están sirviendo para algo: si ambas coinciden, no te estás saltando ningún trabajo.

💡
Un protocolo de medición en cinco pasos

Define la interacción exacta que quieres medir y escríbela para poder repetirla igual. Mide tres veces antes de tocar nada y anota el número, no la impresión. Haz un solo cambio, nunca dos a la vez. Mide otras tres veces en las mismas condiciones. Y decide con la diferencia delante: si el cambio no movió el número, revierte, aunque el código te parezca más elegante con él. Lo que hace ingeniería a este protocolo no es su sofisticación, que es nula, sino el compromiso previo de revertir cuando la medición no acompaña.

Para el caso concreto de este nivel existe una medida todavía más directa que los renders, y es el número de veces que se ejecuta un selector y el número de veces que su cálculo se recomputa de verdad. Ese par de cifras, contrastado con el número de despachos de la interacción, da la tasa de aciertos de tus cachés y localiza el problema sin ambigüedad: muchas ejecuciones con pocos recomputos indica selectores sanos; tantos recomputos como despachos indica una caché que no acierta nunca.

// Tres contadores bastan para diagnosticar toda una pantalla.
let despachos = 0, ejecuciones = 0, recomputos = 0

const medir = () => (next) => (accion) => { despachos += 1; return next(accion) }
// ejecuciones se incrementa en el cuerpo del selector,
// recomputos dentro de la funcion de resultado del selector memoizado.

Los cuatro sesgos que invalidan una medición

Una medición mal montada es peor que ninguna, porque produce confianza sin información. Hay cuatro sesgos que aparecen una y otra vez y conviene descartarlos antes de creerse cualquier número. El primero es medir en modo desarrollo, donde el código no está optimizado, las comprobaciones extra corren y las advertencias se calculan: las cifras absolutas de ese entorno no dicen casi nada del comportamiento real en producción.

El segundo es la duplicación deliberada de StrictMode, que en desarrollo ejecuta dos veces los renders y ciertos efectos para exponer impurezas. Si cuentas renders sin saberlo, verás el doble y perseguirás un problema inexistente; si lo sabes, la duplicación es inofensiva porque afecta igual al antes y al después. El tercero es medir con datos de juguete: una lista de diez elementos no revela nada de una de diez mil, y casi todos los problemas de este nivel son proporcionales al volumen.

⚠️
El cuarto sesgo es tu propia máquina

Mides en un equipo de desarrollo potente, con la aplicación cargada en caliente y una red local. Tus usuarios abren la misma pantalla en teléfonos de gama media, con la batería a media carga y varias pestañas compitiendo. La ralentización artificial de la CPU que ofrecen las herramientas del navegador —cuatro o seis veces— es la corrección mínima antes de declarar que algo es lo bastante rápido. Y la medición que de verdad manda no es la de tu escritorio, sino la de campo: la latencia real de interacción que registran los usuarios, que es la única que integra dispositivo, red y contexto de uso.

flowchart TD
S[algo va lento] --> M1[medir y localizar la causa]
M1 --> C{la causa es el selector}
C -->|si| O1[estrechar memoizar o comparar]
C -->|no| O2[revisar el arbol o memoizar componentes]
O1 --> M2[medir de nuevo la misma interaccion]
O2 --> M2
M2 --> D{mejoro el numero}
D -->|si| K[conservar el cambio]
D -->|no| R[revertir el cambio]
style K fill:#a6e3a1,color:#11111b
style R fill:#f38ba8,color:#11111b

Presupuestos y evidencia de campo

Una medición sin presupuesto no decide nada, porque un número aislado no es rápido ni lento: solo lo es frente a un umbral. El primer presupuesto que conviene interiorizar es el fotograma, unos dieciséis milisegundos y medio a sesenta imágenes por segundo, que es el tiempo total del que dispone el hilo principal para hacer todo lo suyo antes de que el usuario perciba un salto. El segundo es el de respuesta a una interacción, donde el umbral que se considera bueno en las métricas de experiencia actuales ronda los doscientos milisegundos entre la pulsación y el siguiente pintado.

⏱️

El fotograma

Unos 16 ms para todo el trabajo de un cuadro. Si tu render se come la mitad, cualquier animación simultánea empieza a saltar.

👆

La interacción

El tiempo entre la pulsación y el pintado que la refleja. Es la métrica que el usuario percibe como fluidez, y la que se mide en campo.

🧪

Laboratorio

Profiler, ralentización de CPU, datos sintéticos. Sirve para diagnosticar y comparar antes y después, no para saber cómo va en el mundo.

🌍

Campo

Métricas reales de usuarios, con sus dispositivos y sus redes. Es la única evidencia que responde a la pregunta de si el problema existía.

La distinción entre laboratorio y campo ordena todo el trabajo de esta lección. El campo te dice si hay un problema y a cuánta gente afecta; el laboratorio te dice cuál es su causa y si tu cambio la corrige. Invertir el orden es el error metodológico más caro que se comete en rendimiento: pasar semanas afinando en el laboratorio una pantalla que ningún usuario percibía lenta, mientras la que sí lo estaba no aparecía en tu perfil porque tu máquina la disimulaba.

📝
Registrar la medición vale tanto como hacerla

Una medición que no queda escrita se pierde y hay que repetirla, y lo que es peor, deja el cambio huérfano de justificación. Anota en el mensaje del cambio o en un comentario breve la interacción medida, el número de antes, el número de después y las condiciones. Ese registro convierte una optimización en una decisión auditable: dentro de un año, cuando alguien se pregunte por qué ese selector tiene una factory y un comparador propio, encontrará la respuesta en lugar de conservarlo por miedo. La documentación de rendimiento no es la explicación de la técnica, es la evidencia de que hacía falta.

Queda un último punto, el que cierra el nivel entero: toda optimización tiene un coste permanente y un beneficio hipotético. Una memoización añade una caché con su memoria y su posible obsolescencia; un componente memoizado añade una comparación en cada render y una restricción sobre cómo se le pasan las propiedades; un selector partido en cinco añade indirecciones que el siguiente lector deberá reconstruir. Ese coste se paga siempre, en cada línea leída y en cada bug futuro. El beneficio solo existe si la medición lo confirma.

Optimizar sin medir no es imprudencia: es programar contra un modelo que sabes falso

La máxima sobre la optimización prematura se cita tanto que ha perdido su filo, y suele entenderse como un consejo de prudencia o de orden de trabajo, cuando en realidad es una afirmación epistemológica sobre los límites de lo que un programador puede saber leyendo su propio código. Tú razonas sobre coste con un modelo mental construido a partir del texto fuente: este bucle recorre mil elementos, esta función parece cara, aquí se crea un objeto. El coste real lo produce una pila que tu modelo no contiene —un compilador que especula sobre las formas de tus objetos y desoptimiza cuando fallan, un recolector que decide cuándo pagar sus pausas, un reconciliador cuyo trabajo depende de la forma del árbol y no de tu algoritmo, un navegador que reparte hilos, y un dispositivo con una jerarquía de memoria que hace que el patrón de acceso pese más que el número de operaciones—. Entre tu modelo y esa realidad no hay una desviación pequeña que la experiencia corrija con los años: hay una diferencia de varios órdenes de magnitud, que además cambia de signo entre versiones del motor. Medir no es entonces una buena práctica que añade rigor a una intuición ya razonable; es la única vía de acceso a una información que el código, por su naturaleza, no contiene. De ahí se sigue la asimetría que debería gobernar tu conducta: como el coste de una optimización es cierto, inmediato y permanente —complejidad que todo lector futuro paga— y su beneficio es incierto hasta que se comprueba, la carga de la prueba recae siempre sobre quien propone optimizar, nunca sobre quien propone dejar el código simple. Y por eso el ingeniero maduro no es el que conoce más técnicas de este nivel, sino el que, teniéndolas todas a mano, se resiste a aplicarlas hasta que un número le obliga, y las retira sin apego cuando un segundo número demuestra que no sirvieron. El arsenal se juzga por lo que evita, no por lo que despliega.

⚔️ Convierte una sospecha en un número
  1. Elige la pantalla que percibes más lenta y activa la señalización visual de renders; describe con palabras qué se repinta al tocar un solo control.
  2. Graba esa interacción con el profiler y anota los tres componentes con mayor coste acumulado, no los de mayor coste por render.
  3. Activa el registro del motivo de cada render y clasifica los tres casos en problema de selector o problema de padre.
  4. Instrumenta el subárbol sospechoso con el componente Profiler y compara la duración real con la duración base para saber si tus memoizaciones sirven.
  5. Repite la medición con la CPU ralentizada cuatro veces y con un volumen de datos realista; comprueba si el diagnóstico se mantiene.
  6. Aplica un único cambio de las lecciones anteriores, vuelve a medir con el mismo protocolo y revierte si el número no se movió. Escribe la conclusión en una frase.