wandres.dev
DEPURAR LA REACTIVIDAD · por qué no se actualiza

Herramientas: DevTools, profiler y el grafo de dependencias

Las dos lecciones anteriores construyeron el razonamiento; esta entrega el instrumental que lo acelera y explica qué mide realmente cada aparato, que es lo que casi nadie sabe. El profiler de React no dice por qué se renderizó algo sino cuánto costó y qué lo desencadenó, y su columna de causas es el atajo que responde el nodo de la dependencia sobrante en un clic. Las DevTools de Redux no son un visor de estado sino un registro auditable de intenciones, y su viaje en el tiempo funciona porque las transiciones son puras. En los sistemas de grano fino el objeto de estudio cambia por completo: no hay renders que contar, hay un grafo de nodos y aristas que se inspecciona nodo a nodo. La lección cierra con lo que hacer cuando no hay herramienta: envolver la fuente para registrar cada lectura y cada escritura y reconstruir el grafo a mano en veinte líneas.

⏱ 20 min

Un depurador con método y sin herramientas es lento; un depurador con herramientas y sin método es peligroso, porque las herramientas producen números con enorme autoridad y esos números responden preguntas que quizá no son la tuya. Esta lección entrega el instrumental que acelera el árbol de diagnóstico, pero antes de cada aparato explica qué mide exactamente y qué no mide, que es la parte que casi nadie se molesta en aprender y la que separa una sesión de depuración de una colección de capturas de pantalla. Verás que el profiler de React no responde a la pregunta que su nombre popular sugiere, que las DevTools de Redux no son un visor de estado sino un registro de intenciones, y que en los sistemas de grano fino el objeto de estudio deja de ser el render y pasa a ser el grafo, con lo que casi todo el instrumental anterior queda sin objeto. Y verás, al final, lo más importante: cómo fabricar tu propio instrumento en veinte líneas cuando el ecosistema no te da ninguno, porque el grafo de dependencias siempre se puede reconstruir instrumentando sus dos únicas operaciones.

🎯 Al terminar esta lección sabrás
  • Interpretar el profiler sabiendo qué magnitud mide y qué pregunta responde realmente su columna de causas.
  • Usar el registro de acciones como historia auditable y entender por qué el viaje en el tiempo exige transiciones puras.
  • Inspeccionar el grafo de un sistema de grano fino, donde no hay renders que contar sino nodos y aristas.
  • Construir instrumentación propia envolviendo las dos operaciones que definen cualquier grafo reactivo.
⏱️

El profiler mide coste

Dice cuánto tardó cada componente y qué lo desencadenó. No dice si ese trabajo era necesario: eso lo decides tú.

🎬

El log mide intención

Cada acción es una frase con nombre y carga. El viaje en el tiempo solo funciona si las transiciones son puras.

🕸️

El grafo mide estructura

En grano fino no cuentas renders: recorres nodos y aristas, y el bug es una arista que sobra o que falta.

El profiler: qué mide de verdad y cómo leer su columna de causas

El profiler de React graba una sesión de renders y devuelve, por cada confirmación, qué componentes se ejecutaron, cuánto tardó cada uno y qué lo desencadenó. Esa última columna es la valiosa y la que casi nadie explota: para cada componente reporta si se renderizó porque cambió su estado propio, porque cambió un contexto que consume, porque cambió una prop concreta —y la nombra— o simplemente porque su padre se renderizó. Esa lista es la respuesta directa al nodo de la dependencia sobrante de la lección anterior, obtenida sin escribir una línea de instrumentación.

Hay una segunda lectura del profiler que muy poca gente usa y que resulta más informativa que la primera: el gráfico de llamas comparado entre dos confirmaciones consecutivas. No mires la forma absoluta de cada una, mira qué ramas aparecen en ambas. Una rama que se repite idéntica en confirmaciones sucesivas es trabajo que se está rehaciendo sin que su entrada haya cambiado, y esa es la definición operativa de una arista sobrante. La forma del gráfico también diagnostica por sí sola: una franja ancha y plana señala un padre que arrastra a decenas de hijos y apunta a una prop inestable en el nivel superior, mientras que una columna estrecha y profunda señala una cadena de dependencias encadenadas y apunta a una cascada.

Lo que el profiler no puede decirte es si el trabajo era necesario. Mide coste, no legitimidad. Un componente que tarda doce milisegundos puede estar perfectamente justificado y otro que tarda medio milisegundo puede ser puro desperdicio repetido doscientas veces. Por eso la lectura correcta nunca empieza por el componente más lento: empieza por el patrón de repetición. Busca el componente que aparece en más confirmaciones consecutivas con la misma causa declarada, porque ahí es donde una arista sobrante se está cobrando su renta en cada interacción.

💡
Ordena por número de apariciones, no por duración

El instinto ante un panel de rendimiento es ordenar por tiempo descendente y atacar el pico. Para problemas de reactividad ese instinto es contraproducente, porque los picos suelen ser trabajo legítimo y caro —una lista grande que se monta por primera vez, una tabla que se ordena— mientras que las aristas sobrantes se manifiestan como muchísimas ejecuciones baratas. Cincuenta renders de medio milisegundo suman más que uno de veinte y, a diferencia de aquel, son íntegramente eliminables. La métrica que buscas es la frecuencia con causa repetida, no la duración máxima. Cuando el mismo componente aparece en veinte confirmaciones seguidas atribuido a la misma prop, has encontrado el bug aunque su coste individual sea invisible.

El registro de acciones: una historia auditable, no un visor de estado

Las DevTools de Redux se describen mal cuando se presentan como una manera de ver el estado. Ver el estado lo hace cualquier traza. Lo que aportan es otra cosa: una historia ordenada de intenciones con nombre, donde cada entrada declara qué se quiso hacer y qué diferencia produjo eso en el árbol de estado. Esa diferencia, mostrada campo a campo, es lo que convierte una pregunta difusa en una pregunta contestada, porque el fallo de estado casi siempre se ve como una acción que no aparece cuando debería, una que aparece dos veces, o una cuya diferencia no es la que su nombre prometía.

El viaje en el tiempo —retroceder a un estado anterior y volver a avanzar— parece magia y no lo es: funciona porque las transiciones son funciones puras del estado previo y la acción, de modo que reproducir el registro desde el principio reconstruye cualquier punto intermedio con exactitud. De ahí se sigue una consecuencia práctica que conviene tener presente antes de confiar en la herramienta: si tus reducers leen la hora, generan identificadores aleatorios o llaman a la red, el viaje en el tiempo miente. No es un defecto de las DevTools; es que has roto la premisa que las hacía posibles.

// Instrumentacion equivalente para un store que no tenga DevTools propias.
function conRegistro<T>(crear: StateCreator<T>): StateCreator<T> {
  return (set, get, api) =>
    crear((parcial, reemplazar, nombre) => {
      const antes = get()
      set(parcial, reemplazar)
      console.groupCollapsed('accion', nombre ?? 'anonima')
      console.log('antes', antes, 'despues', get())
      console.trace('origen de la escritura')
      console.groupEnd()
    }, get, api)
}

La traza de origen incluida ahí resuelve el caso que más tiempo consume en aplicaciones grandes: la escritura correcta que nadie sabe quién disparó. Con la pila de llamadas capturada en el momento de escribir, la pregunta se responde en un vistazo en lugar de mediante una búsqueda a ciegas por todo el repositorio.

Conviene además explotar una capacidad del registro que casi nadie usa y que convierte una sesión de depuración en un artefacto compartible: la exportación de la secuencia completa de acciones. Un registro exportado es una reproducción determinista del fallo que otro compañero puede importar en su propia máquina para llegar exactamente al mismo estado, sin necesidad de reproducir tu sesión, tus datos ni tu cuenta. Es el equivalente de estado del volcado de memoria en un lenguaje de sistemas, y es la razón por la que merece la pena mantener las acciones con nombres descriptivos: un registro cuyas entradas se llaman actualizar o establecer no reproduce ninguna intención y no sirve de informe para nadie.

Grano fino: el objeto de estudio deja de ser el render

En Solid, en Vue con signals o en cualquier sistema de grano fino, casi todo el instrumental anterior pierde sentido, y no porque sea peor sino porque el fenómeno que medía ha desaparecido. El cuerpo del componente se ejecuta una única vez, así que contar renders no informa de nada. Lo que existe es un grafo persistente de fuentes, derivaciones y efectos, y la depuración consiste en recorrerlo preguntando por las aristas de cada nodo: quién me lee y a quién leo.

// Instrumentar un nodo concreto para ver cuando se recalcula y por que.
const totalTrazado = createMemo(() => {
  const valor = lineas().reduce((s, l) => s + l.precio, 0)
  console.log('recalculo del total', valor, 'lineas:', lineas().length)
  return valor
})

// Ver la propagacion completa de una escritura: cuantos efectos despierta.
batch(() => {
  console.time('propagacion')
  setLineas([...lineas(), nueva])
  console.timeEnd('propagacion')
})

Este cambio de objeto de estudio tiene una consecuencia que sorprende a quien llega desde React: en grano fino, el bug de rendimiento típico deja de ser el trabajo repetido y pasa a ser el trabajo mal ubicado. Como el sistema recalcula únicamente los nodos afectados, casi nunca sobra ejecución; lo que sobra es una derivación colocada demasiado arriba en el grafo, de modo que un cambio pequeño obliga a recomponer una estructura grande. La instrumentación correcta, por tanto, no cuenta ejecuciones sino que mide cuánto trabajo hace cada recálculo, y la corrección no consiste en memoizar sino en partir la derivación en varias más pequeñas y más cercanas a sus fuentes.

La pregunta que sustituye a por qué se renderizó es por qué se recalculó este nodo, y se responde imprimiendo dentro de la derivación sospechosa las fuentes que lee. Si una derivación se recalcula cuando ninguna de sus fuentes cambió de valor, tienes una arista sobrante exactamente igual que en la lección anterior; si no se recalcula cuando una sí cambió, tienes la suscripción ausente de la lección primera. El árbol de diagnóstico es el mismo, cambia el aparato con el que se responde cada nodo.

flowchart LR
S[que sistema tengo delante] --> R[reejecuta el componente]
S --> G[grano fino con grafo persistente]
R --> P[profiler: causa por render y frecuencia]
R --> L[registro de acciones: intencion y diferencia]
G --> N[trazar el nodo: fuentes leidas y recalculos]
P --> D[misma pregunta del arbol de diagnostico]
L --> D
N --> D
style S fill:#cba6f7,color:#11111b
style D fill:#a6e3a1,color:#11111b
style R fill:#89b4fa,color:#11111b
style G fill:#f9e2af,color:#11111b

Sin herramienta: reconstruir el grafo a mano

Hay un cuarto instrumento que no es una extensión y que resuelve la clase de fallos que ninguna de las anteriores alcanza: la marca temporal. Cuando el problema no es cuánto trabajo se hace sino en qué orden ocurren las cosas —una escritura que llega antes de que su suscriptor exista, dos actualizaciones que se pisan, un efecto que corre antes de que el estado se haya asentado—, lo que necesitas es una línea de tiempo, no un recuento. Sembrar marcas en los puntos críticos y leerlas en la pista de rendimiento del navegador convierte una sospecha de carrera en una secuencia observable, y es el único modo honesto de depurar el orden en un sistema donde la propagación es asíncrona.

Habrá casos en los que no exista extensión, el entorno sea de producción o el estado viva en una librería sin instrumentación. No importa: cualquier grafo reactivo se define por dos operaciones, el registro de una lectura y la notificación de una escritura, y envolver ambas basta para reconstruirlo entero. Es el mismo par de operaciones gemelas que localizaba el núcleo de cualquier librería, usado ahora para observarlo desde fuera.

// Envolver la fuente para registrar cada lectura y cada escritura con su origen.
function trazar<T extends object>(fuente: T, nombre: string): T {
  return new Proxy(fuente, {
    get(obj, clave) {
      console.debug('lectura', nombre, String(clave))
      return Reflect.get(obj, clave)
    },
    set(obj, clave, valor) {
      console.debug('escritura', nombre, String(clave), valor, new Error().stack)
      return Reflect.set(obj, clave, valor)
    },
  })
}

Con esa envoltura activa durante una interacción tienes el material bruto para el grafo: cada lectura te dice quién depende de qué y cada escritura te dice quién dispara qué, con la pila que lo originó. Agrupar esas trazas por clave produce, en un minuto, un mapa de aristas que ninguna extensión te habría dado con más precisión. Es tosco, es ruidoso y funciona en cualquier framework de cualquier año.

📝
Instrumentar cambia lo que mides, y a veces lo cura

Toda instrumentación es intrusiva y conviene tenerlo presente antes de sacar conclusiones. Una envoltura basada en Proxy cambia la identidad del objeto observado, lo que puede alterar precisamente las comparaciones por referencia que estás investigando; una traza síncrona dentro de un efecto modifica los tiempos y puede reordenar lo que ocurría en paralelo; y la propia extensión de depuración retiene referencias a estados antiguos para permitir el viaje en el tiempo, con lo que falsea cualquier medición de memoria mientras esté abierta. Si un bug desaparece al instrumentarlo, no lo has arreglado: has descubierto que dependía de un tiempo, de una identidad o de un orden que tu medición perturbó. Ese resultado es información valiosísima —te dice que el fallo es de carrera o de identidad y no de lógica— siempre que lo interpretes así en lugar de darlo por resuelto.

La herramienta no encuentra el bug: acelera la pregunta que tú ya sabías hacer

Conviene decirlo sin adornos porque es el error más caro de esta parte del track: ninguna de estas herramientas encuentra un bug. Todas responden preguntas, y la calidad de la respuesta está acotada por la calidad de la pregunta que traías. Quien abre el profiler sin hipótesis obtiene una lista de componentes ordenados por milisegundos y no sabe qué hacer con ella, porque la lista no contiene la información que buscaba: contiene coste, y él buscaba causa. Quien abre el mismo panel con el árbol de diagnóstico en la cabeza ya sabe que va a preguntar por la causa declarada de un componente concreto y encuentra la respuesta en el primer clic. La diferencia entre los dos no es la herramienta, que es idéntica, sino que uno tenía una pregunta y el otro tenía esperanza. De ahí se sigue el orden correcto de trabajo, que es exactamente el inverso al que practica casi todo el mundo: primero formula la hipótesis con el árbol, después elige el instrumento que la confirma o la refuta, y solo entonces abre el panel. Y de ahí se sigue también por qué merece la pena aprender a fabricar instrumentación propia aunque existan extensiones excelentes: quien sabe envolver la lectura y la escritura para reconstruir un grafo ha entendido que toda esta industria de herramientas no hace nada más sofisticado que eso, con mejor interfaz. Ese entendimiento es lo que te vuelve independiente del ecosistema. Las extensiones cambian de nombre, se abandonan, dejan de soportar la versión mayor que acabas de adoptar; las dos operaciones que definen un grafo reactivo llevan treinta años siendo las mismas y no van a cambiar. Aprende el aparato, pero aprende sobre todo qué mide, porque el día que no lo tengas necesitarás construirlo.

⚔️ Monta tu banco de instrumentos y compáralos
  1. Graba una sesión del profiler sobre una interacción real y ordena los resultados por número de apariciones con la misma causa, no por duración.
  2. Toma el componente que encabece esa lista y predice, antes de mirar la columna de causas, qué dependencia lo está despertando.
  3. Instala la envoltura de registro sobre tu store y comprueba que la traza de origen identifica al disparador de una escritura que no sabías de dónde venía.
  4. Verifica la premisa del viaje en el tiempo: mete una llamada a la hora del sistema dentro de un reducer y observa cómo la reproducción del registro deja de ser fiel.
  5. Si trabajas con grano fino, instrumenta una derivación sospechosa e identifica todas las fuentes que lee frente a las que declara necesitar.
  6. Reconstruye a mano, con la envoltura de trazado, el grafo de una interacción completa y compáralo con lo que creías que era el grafo antes de medirlo.