wandres.dev
SOURCES II · Breakpoints sin línea

Breakpoints de DOM: quién ha tocado mi nodo

Los tres tipos de punto de parada sobre el árbol, qué captura exactamente cada uno, y los casos donde resuelven en dos minutos lo que de otra forma cuesta una tarde.

⏱ 18 min

De todas las herramientas de las DevTools, esta es la que tiene la mayor distancia entre lo poco que se conoce y lo mucho que resuelve. Un breakpoint de DOM detiene la ejecución en el instante exacto en que un nodo concreto cambia, con la pila de llamadas apuntando al culpable, funcione como funcione el código que lo modifica y esté donde esté. Preguntas que consumen tardes enteras —quién pone esta clase, quién borra este elemento, quién me está cambiando el estilo en línea— se responden aquí en menos de dos minutos.

🎯 Al terminar esta lección sabrás
  • Distinguir los tres tipos de breakpoint de DOM y qué mutación captura cada uno.
  • Colocarlos en el nodo correcto, que casi nunca es el nodo donde ves el efecto.
  • Resolver los cinco casos canónicos de esta familia.
  • Reconocer las tres situaciones donde un breakpoint de DOM no se dispara y qué usar entonces.

Los tres tipos

Se ponen con el botón derecho sobre un nodo en el panel de Elements, en el submenú de puntos de parada.

Modificaciones del subárbol. Se dispara cuando se añade o se elimina un hijo del nodo, a cualquier profundidad. No se dispara por cambios de atributos ni de texto de los descendientes. Es el más amplio y el que más ruido produce en aplicaciones reactivas.

Modificaciones de atributos. Se dispara cuando se añade, se cambia o se elimina un atributo del nodo en el que lo pones, no de sus descendientes. Cubre class, style, id, data-, disabled, aria- y todo lo demás. Es el más útil de los tres con diferencia.

Eliminación del nodo. Se dispara cuando el nodo se saca del documento. Responde a “quién ha borrado esto” y solo a eso.

Los tres se listan en el subpanel de breakpoints de DOM con el nodo al que pertenecen, y se pueden desactivar sin borrarlos igual que los de línea.

⚠️
Cuidado

Los breakpoints de DOM están atados al nodo, no al selector. Si el nodo desaparece del documento —porque un framework lo sustituyó por otro equivalente en un render— el breakpoint se pierde. En aplicaciones reactivas, esto significa que a menudo hay que ponerlo en un ancestro estable en vez de en el nodo que te interesa.

Poner el breakpoint en el nodo correcto

Es el error de principiante de esta familia y merece un procedimiento. Los tres casos.

Si vigilas un cambio de clase o de estilo, el breakpoint va en ese mismo nodo, de modificación de atributos. Directo.

Si vigilas la aparición de un elemento, el breakpoint va en el padre donde va a aparecer, de modificación de subárbol. El elemento todavía no existe, así que no puedes ponerle nada.

Si vigilas la desaparición de un elemento, hay dos opciones y la elección importa. El breakpoint de eliminación en el propio nodo se dispara cuando lo quitan directamente. Pero si lo que ocurre es que el padre entero se reemplaza, el hijo se va sin que nadie lo elimine explícitamente, y el breakpoint no salta. En ese caso hay que ponerlo en el padre, de modificación de subárbol, o subir un nivel más.

Ese último matiz explica la mayoría de los “lo he puesto y no se dispara”.

Los cinco casos canónicos

Caso uno: la clase que aparece de la nada. Un elemento adquiere una clase que activa un estilo que no quieres, y buscar el nombre de la clase en el proyecto devuelve treinta resultados irrelevantes. Breakpoint de modificación de atributos en ese nodo, reproducir. La ejecución se detiene en la línea exacta que hace la asignación, y las DevTools muestran además, sobre el editor, qué atributo cambió y de qué valor a qué valor. Funciona aunque el nombre de la clase se construya concatenando cadenas, aunque venga de un mapa de configuración, y aunque la asignación esté dentro de una librería minificada.

Caso dos: el estilo en línea misterioso. Un elemento tiene un style con valores que no están en ningún fichero. El mismo breakpoint de atributos lo caza. La causa suele ser una librería de posicionamiento, un componente de animación, o código de accesibilidad que oculta cosas.

Caso tres: el nodo que desaparece al interactuar. Un elemento se va y no hay nada en tu código que lo quite. Breakpoint de eliminación de nodo, y si no salta, breakpoint de modificación de subárbol en el padre. La causa habitual es un render del framework que reconstruye la sección entera.

Caso cuatro: los elementos duplicados. Una lista muestra cada elemento dos veces. Breakpoint de modificación de subárbol en el contenedor. Te detendrás en cada inserción, y las dos pilas dirán si el mismo código se ejecutó dos veces —listener duplicado, efecto sin limpieza— o si son dos códigos distintos insertando lo mismo.

Caso cinco: quién bloquea el scroll del cuerpo. El scroll de la página se queda bloqueado después de cerrar un modal. Breakpoint de modificación de atributos en el elemento body. Cuando alguien le ponga o le quite el style con overflow: hidden, te enteras. Este caso concreto es de los más frecuentes en aplicaciones con varios modales, y la causa suele ser dos componentes que bloquean y desbloquean sin coordinarse: el segundo en cerrarse desbloquea, el primero vuelve a bloquear al desmontarse, y el orden decide si funciona o no.

Cuando no se dispara

Tres situaciones y su alternativa.

El cambio lo hace CSS, no JavaScript. Si el elemento cambia porque una regla empezó a aplicarse —una media query, un :hover, una animación— no hay código que ejecutar. El breakpoint no salta porque no hay nada que detener. La herramienta correcta es el panel de estilos.

El nodo se sustituye en vez de modificarse. Ya visto. Subir un nivel y usar modificación de subárbol.

El cambio ocurre dentro de un shadow DOM. Los breakpoints funcionan sobre nodos de la raíz de sombra si es abierta y puedes seleccionarla en el árbol. Si es cerrada, no hay acceso.

Complementar con MutationObserver

Cuando el breakpoint produce demasiado ruido —el caso típico es un contenedor que se actualiza sesenta veces por segundo— la alternativa es observar sin detener, y filtrar.

// Traza solo los cambios de clase de este nodo, con la pila de quien los hizo
new MutationObserver(mutaciones => {
  for (const m of mutaciones) {
    if (m.attributeName === 'class') {
      console.groupCollapsed('class:', m.oldValue, '->', m.target.className);
      console.trace();
      console.groupEnd();
    }
  }
}).observe($0, { attributes: true, attributeOldValue: true });
// Detener solo cuando aparezca una clase concreta
new MutationObserver(ms => {
  for (const m of ms) {
    if (m.target.classList.contains('oculto')) { debugger; }
  }
}).observe($0, { attributes: true, attributeFilter: ['class'] });

La segunda versión es un breakpoint de DOM condicional, que es algo que la interfaz no ofrece directamente. Con un MutationObserver y un debugger se consigue exactamente eso, y en aplicaciones muy reactivas es la única forma de que la técnica sea usable.

El breakpoint de DOM detiene en la mutación, y en un framework la mutación no es donde está tu bug

Hay un matiz que separa usar esta herramienta con soltura de usarla y frustrarse, y tiene que ver con dónde vive la causa en una aplicación moderna. Cuando un framework declarativo actualiza el DOM, la mutación la ejecuta su capa de renderizado, en respuesta a un cambio de estado que ocurrió antes, posiblemente varios ciclos antes y desde código completamente distinto. El breakpoint de DOM te detiene en la mutación, es decir, en el último eslabón de esa cadena, y ahí encontrarás el reconciliador aplicando un cambio que ya estaba decidido. Tu bug no está ahí: está en quien cambió el estado. La reacción de mucha gente ante eso es concluir que la herramienta no sirve para su framework, y es una conclusión equivocada por una razón concreta: la pila asíncrona sí llega hasta el origen. Con las pilas asíncronas activadas, el marco que provocó la actualización —tu manejador, tu efecto, tu respuesta de red— aparece en la parte baja de la pila, separado por un divisor que indica el salto asíncrono. Ahí está la causa. Así que el procedimiento correcto con framework tiene un paso más que sin él: te detienes en la mutación, ignoras por completo los marcos superiores, bajas hasta el primer marco tuyo o hasta la sección asíncrona, y ese es tu punto de partida real. Y hay un atajo que conviene tener a mano para el caso más frecuente de todos: cuando lo que quieres saber es qué cambio de estado provocó un render, a menudo es más rápido poner el breakpoint en tu propia función que modifica el estado —el reductor, el setState, la asignación a la señal— con una condición sobre el valor que te interesa, que perseguir la mutación del DOM hacia atrás. La regla que decide entre las dos vías: usa el breakpoint de DOM cuando no sepas qué código es responsable, y el de línea sobre el estado cuando ya lo sepas. El primero es para descubrir; el segundo, para trabajar.