Snippets de red, almacenamiento y observación
Interceptar peticiones, auditar cookies y almacenamiento, medir la cuota, y observar mutaciones con filtro, todo desde la consola.
Los paneles de red y de aplicación son excelentes para explorar y torpes para consultar. Cuando la pregunta es “cuántas peticiones van a este dominio”, “qué clave del almacenamiento ocupa más” o “quién está modificando este subárbol y con qué frecuencia”, una expresión responde en un segundo lo que en el panel requiere ordenar, filtrar y contar a ojo. Estas cinco herramientas cubren esas consultas.
- Interceptar y registrar todas las peticiones de la página con su origen y su duración.
- Auditar el almacenamiento local y de sesión por tamaño de cada clave.
- Consultar las cookies con todos sus atributos y detectar las problemáticas.
- Observar mutaciones de un subárbol con filtro y con desactivación limpia.
Interceptor de red completo
Registra todo lo que sale, con método, estado, duración, tamaño y la pila de quien lo pidió. Cubre tanto fetch como la API antigua.
// Interceptor de red con registro y resumen
(() => {
const CLAVE = '__red';
if (globalThis[CLAVE]) { globalThis[CLAVE](); delete globalThis[CLAVE]; return; }
const registro = [];
const fetchOriginal = window.fetch;
const abrirOriginal = XMLHttpRequest.prototype.open;
const enviarOriginal = XMLHttpRequest.prototype.send;
const anota = (metodo, url, inicio, estado, error) => {
const fila = { metodo, url: String(url).split('?')[0], ms: Math.round(performance.now() - inicio), estado: estado ?? (error ? 'error' : '') };
registro.push(fila);
console.debug(`%c${metodo}%c ${fila.estado} ${fila.ms}ms ${fila.url}`,
`background:${error ? '#f38ba8' : '#89b4fa'};color:#11111b;padding:1px 5px;border-radius:3px`, '');
};
window.fetch = function (recurso, opciones = {}) {
const url = recurso instanceof Request ? recurso.url : recurso;
const metodo = (opciones.method || (recurso instanceof Request ? recurso.method : 'GET')).toUpperCase();
const inicio = performance.now();
return fetchOriginal.apply(this, arguments)
.then(r => { anota(metodo, url, inicio, r.status); return r; })
.catch(e => { anota(metodo, url, inicio, null, e); throw e; });
};
XMLHttpRequest.prototype.open = function (metodo, url) {
this.__info = { metodo: String(metodo).toUpperCase(), url };
return abrirOriginal.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
const inicio = performance.now();
this.addEventListener('loadend', () => anota(this.__info.metodo, this.__info.url, inicio, this.status));
return enviarOriginal.apply(this, arguments);
};
globalThis[CLAVE] = () => {
window.fetch = fetchOriginal;
XMLHttpRequest.prototype.open = abrirOriginal;
XMLHttpRequest.prototype.send = enviarOriginal;
console.table(registro);
const porUrl = {};
registro.forEach(r => { porUrl[r.url] = (porUrl[r.url] || 0) + 1; });
const repetidas = Object.entries(porUrl).filter(([, n]) => n > 1).sort((a, b) => b[1] - a[1]);
if (repetidas.length) console.warn('URLs pedidas mas de una vez:', Object.fromEntries(repetidas));
};
console.log('interceptor de red activo; vuelve a ejecutar para pararlo y ver el resumen');
})();
El aviso final sobre URLs repetidas es la parte más útil: las peticiones duplicadas son uno de los defectos más comunes y más invisibles de las aplicaciones modernas, y esta herramienta las lista solas.
Auditoría del almacenamiento
Las claves con su tamaño, ordenadas, más el total y la cuota.
// Que hay en localStorage y sessionStorage, y cuanto ocupa
(async () => {
const auditar = (almacen, nombre) => {
const filas = Object.keys(almacen).map(k => {
const v = almacen.getItem(k) ?? '';
let tipo = 'texto';
try { const p = JSON.parse(v); tipo = Array.isArray(p) ? `array[${p.length}]` : typeof p === 'object' && p ? `objeto{${Object.keys(p).length}}` : typeof p; } catch {}
return { clave: k.slice(0, 45), tipo, bytes: new Blob([k + v]).size, muestra: v.slice(0, 45) };
}).sort((a, b) => b.bytes - a.bytes);
const total = filas.reduce((s, f) => s + f.bytes, 0);
console.log(`%c${nombre}%c ${filas.length} claves, ${(total / 1024).toFixed(1)} kB`,
'background:#94e2d5;color:#11111b;padding:1px 5px;border-radius:3px', '');
if (filas.length) console.table(filas);
return total;
};
auditar(localStorage, 'localStorage');
auditar(sessionStorage, 'sessionStorage');
if (navigator.storage?.estimate) {
const e = await navigator.storage.estimate();
console.table([{
usadoMB: (e.usage / 1048576).toFixed(2),
cuotaMB: (e.quota / 1048576).toFixed(0),
porcentaje: (e.usage / e.quota * 100).toFixed(3) + '%',
persistente: await navigator.storage.persisted?.()
}]);
}
})();
El desglose por tipo detecta el problema clásico del almacenamiento local: alguien guardó un array de dos mil objetos serializado como cadena, y ahora cada lectura implica parsear varios megabytes de JSON de forma síncrona en el hilo principal.
localStorage es síncrono y bloquea el hilo principal tanto al leer como al escribir. Una clave de un megabyte leída en cada render es una fuente de tareas largas que ninguna herramienta de rendimiento atribuye claramente, porque el tiempo aparece dentro de tu propia función. Este snippet la encuentra en un segundo.
Las cookies con todos sus atributos
document.cookie solo da nombres y valores. La API de almacenamiento de cookies, cuando está disponible, da todo lo demás.
// Cookies accesibles desde JavaScript, con atributos si el navegador lo permite
(async () => {
if (globalThis.cookieStore?.getAll) {
const todas = await cookieStore.getAll();
console.table(todas.map(c => ({
nombre: c.name,
dominio: c.domain ?? '(host)',
ruta: c.path,
sameSite: c.sameSite,
seguro: c.secure,
expira: c.expires ? new Date(c.expires).toISOString().slice(0, 16) : 'sesion',
bytes: new Blob([c.name + c.value]).size
})).sort((a, b) => b.bytes - a.bytes));
} else {
console.table(document.cookie.split('; ').filter(Boolean).map(p => {
const i = p.indexOf('=');
return { nombre: p.slice(0, i), bytes: new Blob([p]).size, muestra: p.slice(i + 1, i + 40) };
}).sort((a, b) => b.bytes - a.bytes));
}
const total = new Blob([document.cookie]).size;
console.log('cookies visibles desde JavaScript:', total, 'bytes');
if (total > 4096) console.warn('el conjunto supera los 4 kB y viaja en cada peticion al mismo origen');
})();
El aviso del final es el punto: las cookies se envían con cada petición al dominio, así que un conjunto grande es un impuesto sobre cada carga de cada recurso. Y las marcadas como HttpOnly no aparecen aquí, así que el total real puede ser mayor; el panel de aplicación las muestra todas.
Observador de mutaciones con filtro
Cuando quieres saber quién y con qué frecuencia toca un subárbol, sin detener nada.
// Observa las mutaciones de un subarbol, con resumen por tipo
((raiz = document.body) => {
const CLAVE = '__mut';
if (globalThis[CLAVE]) { globalThis[CLAVE](); delete globalThis[CLAVE]; return; }
const cuenta = { hijos: 0, atributos: 0, texto: 0 };
const porAtributo = {};
const obs = new MutationObserver(ms => {
for (const m of ms) {
if (m.type === 'childList') cuenta.hijos += m.addedNodes.length + m.removedNodes.length;
else if (m.type === 'attributes') { cuenta.atributos++; porAtributo[m.attributeName] = (porAtributo[m.attributeName] || 0) + 1; }
else cuenta.texto++;
}
});
obs.observe(raiz, { childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true });
const id = setInterval(() => {
if (cuenta.hijos + cuenta.atributos + cuenta.texto === 0) return;
console.log('mutaciones en el ultimo segundo:', { ...cuenta }, porAtributo);
cuenta.hijos = cuenta.atributos = cuenta.texto = 0;
}, 1000);
globalThis[CLAVE] = () => { obs.disconnect(); clearInterval(id); };
console.log('observando mutaciones de', raiz, '; vuelve a ejecutar para parar');
})($0 || document.body);
Al usar $0 como raíz por defecto, basta con seleccionar un nodo en el panel de Elements antes de ejecutarlo para vigilar solo esa parte. Un contador que sube constantemente en reposo indica trabajo inútil, que es una de las causas más frecuentes de lentitud silenciosa.
Tres de los snippets de esta lección funcionan sustituyendo una función del navegador por otra que informa y delega, y la técnica es tan útil que merece enunciarse con sus reglas, porque hecha mal produce bugs peores que los que investigabas. Regla uno: guarda el original antes de nada y restáuralo siempre. Si ejecutas dos veces un snippet que envuelve fetch sin comprobación, la segunda envuelve a la primera y ahora tienes dos capas; la tercera hace tres, y el rendimiento y los logs se degradan de forma acumulativa. Por eso todos los snippets de esta guía comprueban una clave global y se desinstalan si ya estaban puestos. Regla dos: preserva el this y todos los argumentos. Usar apply(this, arguments) en vez de llamar con los parámetros que declaraste evita romper los casos que no anticipaste: un fetch llamado con un objeto Request en vez de una cadena, un open de la API antigua llamado con los cinco argumentos de autenticación. Cualquier envoltura que reescriba la firma acaba rompiendo alguna llamada. Regla tres: no cambies el valor de retorno ni tragues excepciones. Una envoltura que devuelve undefined porque olvidaste el return, o que captura un error y no lo relanza, convierte tu instrumento de diagnóstico en la causa del siguiente bug, y ese bug parecerá de la aplicación. Regla cuatro, la que más sorprende: envolver cambia la identidad de la función. Cualquier código que compare funciones por referencia deja de funcionar: un removeEventListener con el manejador original, un mapa de callbacks, una comprobación de igualdad. Si al instalar un instrumento aparece un comportamiento que no estaba antes, esa es la primera sospecha y la comprobación es desinstalar y ver si se va. Con las cuatro reglas, la técnica es segura y abre una puerta que nada más abre: hace observable cualquier API del navegador o de una librería, sin código fuente, sin recompilar y sin permiso de nadie.