wandres.dev
CONSOLE II · Utilidades y contexto

copy, queryObjects y el resto del arsenal de la consola

Las utilidades que quedan: sacar datos al portapapeles, encontrar todas las instancias vivas de una clase, poner breakpoints por código y las funciones auxiliares.

⏱ 15 min

Más allá de la selección, la consola inyecta una segunda tanda de utilidades que resuelven problemas concretos y que casi nadie usa porque no aparecen en ningún autocompletado obvio. Una de ellas, queryObjects, hace algo que ninguna otra herramienta del navegador hace: enumerar todas las instancias vivas de un constructor, lo que la convierte en el detector de fugas más rápido que existe para el caso más frecuente.

🎯 Al terminar esta lección sabrás
  • Extraer datos de la consola al portapapeles en el formato adecuado.
  • Enumerar las instancias vivas de una clase y usarlo para detectar acumulación.
  • Poner y quitar breakpoints en funciones sin abrir el panel de Sources.
  • Conocer las utilidades auxiliares y saber cuándo cada una ahorra código.

copy

Copia su argumento al portapapeles del sistema. Si es un objeto, lo serializa a JSON; si es una cadena, la copia tal cual; si es un nodo del DOM, copia su marcado.

copy($0.outerHTML);
copy(JSON.stringify(datos, null, 2));
copy($$('a').map(a => a.href).join('\n'));

Su valor está en el paso de la consola al exterior. La alternativa —expandir un objeto en la consola, seleccionarlo con el ratón y copiar— produce texto con adornos de la interfaz que hay que limpiar. copy da el dato limpio.

El caso que más rinde es extraer una tabla para una hoja de cálculo.

// Copia como TSV, listo para pegar en cualquier hoja de calculo
function copiarTabla(filas) {
  const cols = Object.keys(filas[0]);
  copy([cols.join('\t'), ...filas.map(f => cols.map(c => f[c]).join('\t'))].join('\n'));
}
copiarTabla(
  performance.getEntriesByType('resource')
    .map(r => ({ url: r.name, tipo: r.initiatorType, ms: Math.round(r.duration), bytes: r.transferSize || 0 }))
);
⚠️
Cuidado

copy requiere que la ventana de las DevTools tenga el foco, y en algunos sistemas puede fallar silenciosamente si se llama desde un contexto donde el navegador no considera que haya habido interacción del usuario. Si el portapapeles queda vacío, ejecutar la expresión otra vez con el foco en la consola suele resolverlo.

queryObjects

Recibe un constructor y devuelve un array con todas las instancias vivas de ese constructor que hay en el montón en ese momento. Antes de enumerar, fuerza una recolección de basura, así que lo que devuelve son objetos que de verdad siguen referenciados por algo.

queryObjects(Promise);
queryObjects(HTMLDivElement);
queryObjects(MiComponente);

Para que funcione con una clase tuya, el constructor tiene que estar accesible desde el contexto de evaluación. Si tu código está empaquetado en módulos, no lo estará; la forma de conseguirlo es tomar una instancia y usar su constructor.

const cualquiera = /* una instancia obtenida como sea, por ejemplo desde un breakpoint */ $0.__vue__ ?? null;
queryObjects(Object.getPrototypeOf(cualquiera).constructor);

O, más práctico, detenerse en un breakpoint dentro de la clase y ejecutar queryObjects(this.constructor) desde ahí.

El uso que lo hace imprescindible es detectar acumulación. El procedimiento es de tres pasos y se puede hacer en un minuto.

queryObjects(MiClase).length;   // apunta el numero
// haz la interaccion sospechosa: abrir y cerrar un modal diez veces
queryObjects(MiClase).length;   // vuelve a mirar

Si el número no vuelve al original después de cerrar todo, tienes instancias que sobreviven a su ciclo de vida, y eso es una fuga. El diagnóstico completo con la ruta de retención vive en el panel de memoria, pero la detección cuesta dos líneas y no requiere ningún snapshot.

Un ejemplo que funciona en cualquier página y que suele sorprender.

// Cuantos nodos desconectados del documento siguen vivos
queryObjects(HTMLElement) && console.log(
  'nodos vivos desconectados:',
  queryObjects(HTMLElement) === undefined ? 'usa el array del resultado anterior' : ''
);

queryObjects imprime su resultado como un array en la consola pero devuelve undefined, así que hay que trabajar con lo que imprime o guardarlo como variable global desde el menú contextual del resultado. Ese detalle desconcierta la primera vez.

debug y undebug

debug(funcion) pone un breakpoint en la primera línea de esa función. undebug(funcion) lo quita. monitor(funcion) y unmonitor(funcion) hacen lo mismo pero en vez de detener, escriben en la consola cada llamada con sus argumentos.

debug(miModulo.procesar);
monitor(window.fetch);

Su valor frente a poner el breakpoint a mano en Sources es que no necesitas encontrar el fichero. Si tienes la referencia a la función, ya está. Y combinado con getEventListeners, permite poner un breakpoint en un manejador concreto sin saber dónde está definido.

debug(getEventListeners($0).click[0].listener);

Esa línea es una de las más rentables de toda la guía: pone un punto de parada exactamente en el código que se ejecuta al pulsar ese botón, sin haber abierto ningún fichero.

Las utilidades auxiliares

keys(objeto) y values(objeto) equivalen a Object.keys y Object.values. Ahorran teclear, y poco más.

clear() vacía la consola. Equivale al botón y al atajo.

inspect(objetoOFuncion) ya visto: lleva un nodo al panel de Elements o una función a Sources.

getAccessibleName y getAccessibleRole, vistas en el nivel de accesibilidad, devuelven promesas con el nombre y el rol computados.

dir y dirxml existen también como utilidades, aunque console.dir es lo habitual.

profile() y profileEnd() inician y detienen una grabación del perfilador de CPU desde código, con el resultado disponible en el panel de rendimiento. Útil cuando quieres perfilar exactamente un intervalo delimitado por condiciones de tu código y no por tus reflejos con el botón de grabar.

queryObjects hace visible el ciclo de vida que tu framework promete y a veces no cumple

Hay una pregunta que se hace muy poca gente y que queryObjects responde en diez segundos: ¿de verdad se destruyen tus componentes cuando el framework dice que se destruyen? Todos los frameworks modernos tienen un ciclo de vida con un paso de desmontaje, y todos documentan que llegado ese punto el componente deja de existir. Lo que ninguno puede garantizar es que tu código haya soltado sus referencias: un setInterval que no se limpió, un listener registrado en window que no se quitó, una suscripción a un almacén global, una promesa pendiente que captura this, una entrada en un mapa de caché indexado por identificador. Cualquiera de esas mantiene vivo el componente entero, con todo su estado y con todos los nodos del DOM que tuviera referenciados, indefinidamente. El síntoma en producción es que la aplicación va perdiendo velocidad conforme el usuario navega, hasta que recarga y vuelve a ir bien, y es de los diagnósticos más difíciles porque no hay ningún error, ningún log y ningún momento concreto en el que falle. El procedimiento con queryObjects es tan sencillo que merece convertirse en costumbre antes de dar por terminada una función: cuenta las instancias, ejecuta el ciclo completo de montaje y desmontaje unas cuantas veces, vuelve a contar. Si el número sube, has encontrado la fuga en el momento en que la introdujiste, en lugar de tres meses después cuando alguien reporte que la aplicación se ralentiza. Y hay una variante del mismo truco que vale para el caso más común de todos y que no necesita ninguna clase propia: contar las instancias de un elemento del DOM concreto que tu componente crea. Si después de cerrar diez modales siguen existiendo diez diálogos vivos, ya sabes lo que pasa aunque no sepas todavía por qué.