wandres.dev
SNIPPETS · Tu caja de herramientas

Snippets de rendimiento y de tamaño

Herramientas para medir el peso de una página sin abrir ningún panel: nodos, recursos, tareas largas, saltos de layout y fotogramas.

⏱ 16 min

Las APIs de rendimiento del navegador exponen prácticamente todo lo que el panel de Performance dibuja, en forma de objetos consultables desde la consola. Eso permite construir instrumentos ligeros que responden a preguntas concretas en un segundo, sin grabar nada, sin esperar a que se procese un perfil y sin la ceremonia de una sesión de análisis. Estas cinco herramientas cubren las preguntas de tamaño y de coste que más se repiten.

🎯 Al terminar esta lección sabrás
  • Medir el tamaño estructural de una página y detectar árboles patológicos.
  • Ordenar los recursos descargados por peso y por duración desde la consola.
  • Instalar observadores de tareas largas y de saltos de layout con limpieza incluida.
  • Medir la tasa de fotogramas durante una interacción concreta.

El tamaño estructural del documento

Un árbol de DOM grande hace lento todo: el cálculo de estilos, el layout, las consultas, el propio panel de Elements. Las recomendaciones habituales hablan de mantenerse por debajo de unos pocos miles de nodos y de una profundidad moderada, pero el número que de verdad importa es el relativo: si tu página tiene diez veces más nodos que hace tres meses, algo ha cambiado.

// Radiografia estructural del documento
(() => {
  const todos = document.querySelectorAll('*');
  let maxProf = 0, nodoMasProfundo = null, maxHijos = 0, padreMasAncho = null;
  const porEtiqueta = {};

  for (const el of todos) {
    porEtiqueta[el.tagName] = (porEtiqueta[el.tagName] || 0) + 1;
    let p = 0;
    for (let n = el; n.parentElement; n = n.parentElement) p++;
    if (p > maxProf) { maxProf = p; nodoMasProfundo = el; }
    if (el.children.length > maxHijos) { maxHijos = el.children.length; padreMasAncho = el; }
  }

  console.table([{
    elementos: todos.length,
    nodosTotales: document.getElementsByTagName('*').length,
    profundidadMaxima: maxProf,
    maxHijosDeUnPadre: maxHijos,
    hojasDeEstilo: document.styleSheets.length,
    reglasCSS: [...document.styleSheets].reduce((s, h) => { try { return s + h.cssRules.length; } catch { return s; } }, 0),
    scripts: document.scripts.length,
    imagenes: document.images.length,
    iframes: document.querySelectorAll('iframe').length
  }]);

  console.log('nodo mas profundo:', nodoMasProfundo);
  console.log('padre con mas hijos:', padreMasAncho);
  console.table(Object.entries(porEtiqueta).sort((a, b) => b[1] - a[1]).slice(0, 12)
    .map(([etiqueta, n]) => ({ etiqueta: etiqueta.toLowerCase(), cuantos: n })));
})();

El try alrededor del recuento de reglas es necesario: acceder a cssRules de una hoja cargada desde otro origen lanza una excepción de seguridad, y sin la protección el snippet fallaría en cualquier página con una fuente externa.

Los recursos por peso y por tiempo

Todo lo que la página ha descargado, con sus tamaños reales y sus tiempos, ordenable.

// Recursos descargados, ordenados por bytes transferidos
(() => {
  const r = performance.getEntriesByType('resource');
  const filas = r.map(e => ({
    recurso: e.name.split('/').pop().split('?')[0].slice(0, 45) || e.name,
    tipo: e.initiatorType,
    protocolo: e.nextHopProtocol || '',
    kbRed: Math.round((e.transferSize || 0) / 1024),
    kbDescomprimido: Math.round((e.decodedBodySize || 0) / 1024),
    ms: Math.round(e.duration),
    cache: e.transferSize === 0 && e.decodedBodySize > 0 ? 'si' : ''
  })).sort((a, b) => b.kbRed - a.kbRed);

  console.table(filas.slice(0, 25));
  const total = filas.reduce((s, f) => s + f.kbRed, 0);
  const porTipo = {};
  filas.forEach(f => { porTipo[f.tipo] = (porTipo[f.tipo] || 0) + f.kbRed; });
  console.table(Object.entries(porTipo).sort((a, b) => b[1] - a[1]).map(([tipo, kb]) => ({ tipo, kb, porcentaje: Math.round(kb / total * 100) + '%' })));
  console.log('total transferido:', total, 'kB en', filas.length, 'recursos');
})();

La columna de caché merece explicación: un recurso con transferSize igual a cero y cuerpo decodificado mayor que cero se sirvió desde la caché sin tocar la red. Ese dato es el que dice si tu estrategia de caché está funcionando.

⚠️
Cuidado

Las entradas de recursos de otro origen exponen sus tamaños solo si el servidor envía la cabecera Timing-Allow-Origin. Sin ella, transferSize y los tiempos detallados aparecen como cero. Un montón de ceros en recursos de un CDN no significa que sean gratis: significa que no te dejan medirlos.

Observadores en vivo

Observador de tareas largas

Una tarea larga es un bloque de trabajo en el hilo principal que supera los cincuenta milisegundos, y es lo que hace que una interfaz se sienta pegajosa. El navegador las reporta.

// Vigila las tareas largas y quien las causa
(() => {
  const CLAVE = '__tareasLargas';
  if (globalThis[CLAVE]) { globalThis[CLAVE](); delete globalThis[CLAVE]; console.log('observador de tareas largas detenido'); return; }

  const registro = [];
  const obs = new PerformanceObserver(lista => {
    for (const e of lista.getEntries()) {
      registro.push(e);
      const origen = e.attribution?.[0];
      console.warn(
        `%ctarea larga%c ${Math.round(e.duration)} ms`,
        'background:#f9e2af;color:#11111b;padding:1px 5px;border-radius:3px', '',
        origen ? `origen: ${origen.name} ${origen.containerType || ''} ${origen.containerName || ''}` : ''
      );
    }
  });
  try { obs.observe({ type: 'longtask', buffered: true }); }
  catch { console.error('este navegador no soporta longtask'); return; }

  globalThis[CLAVE] = () => { obs.disconnect(); console.table(registro.map(e => ({ inicio: Math.round(e.startTime), ms: Math.round(e.duration) }))); };
  console.log('observando tareas largas; vuelve a ejecutar para parar y ver el resumen');
})();

La opción buffered hace que se reporten también las tareas ocurridas antes de instalar el observador, lo cual es lo que quieres si lo instalas después de que la página haya cargado.

La atribución dice si la tarea vino del propio documento o de un iframe, lo cual es la primera bisección cuando hay incrustaciones de terceros.

Observador de saltos de layout

Los desplazamientos inesperados de contenido son de los defectos que más molestan y de los más difíciles de reproducir a posteriori. Este observador registra cada uno con los elementos implicados.

// Registra cada salto de layout con los elementos que se movieron
(() => {
  const CLAVE = '__saltos';
  if (globalThis[CLAVE]) { globalThis[CLAVE](); delete globalThis[CLAVE]; return; }

  let acumulado = 0;
  const obs = new PerformanceObserver(lista => {
    for (const e of lista.getEntries()) {
      if (e.hadRecentInput) continue;          // los provocados por el usuario no cuentan
      acumulado += e.value;
      console.groupCollapsed(`%csalto%c ${e.value.toFixed(4)} (acumulado ${acumulado.toFixed(4)})`,
        'background:#f38ba8;color:#11111b;padding:1px 5px;border-radius:3px', '');
      for (const f of e.sources || []) {
        console.log(f.node, 'de', JSON.stringify(f.previousRect), 'a', JSON.stringify(f.currentRect));
      }
      console.groupEnd();
    }
  });
  try { obs.observe({ type: 'layout-shift', buffered: true }); }
  catch { console.error('este navegador no soporta layout-shift'); return; }

  globalThis[CLAVE] = () => { obs.disconnect(); console.log('acumulado final:', acumulado.toFixed(4)); };
  console.log('observando saltos de layout; vuelve a ejecutar para parar');
})();

Lo valioso aquí es sources, que da el nodo concreto que se movió y sus rectángulos antes y después. Con eso, un salto deja de ser un número abstracto y pasa a ser “esta imagen empujó este párrafo cuarenta píxeles hacia abajo”.

Medidor de fotogramas

Para saber si una animación o un scroll van fluidos durante una interacción concreta.

// Contador de fotogramas por segundo, con indicador flotante
(() => {
  const CLAVE = '__fps';
  if (globalThis[CLAVE]) { globalThis[CLAVE](); delete globalThis[CLAVE]; return; }

  const caja = document.createElement('div');
  caja.style.cssText = 'position:fixed;top:8px;right:8px;z-index:2147483647;background:#11111b;color:#a6e3a1;font:12px/1.5 monospace;padding:4px 8px;border-radius:4px;pointer-events:none';
  document.body.appendChild(caja);

  let cuadros = 0, ultimo = performance.now(), peor = Infinity, id;
  const muestras = [];
  const bucle = ahora => {
    cuadros++;
    if (ahora - ultimo >= 1000) {
      const fps = Math.round(cuadros * 1000 / (ahora - ultimo));
      peor = Math.min(peor, fps);
      muestras.push(fps);
      caja.textContent = `${fps} fps (peor ${peor})`;
      caja.style.color = fps >= 55 ? '#a6e3a1' : fps >= 30 ? '#f9e2af' : '#f38ba8';
      cuadros = 0; ultimo = ahora;
    }
    id = requestAnimationFrame(bucle);
  };
  id = requestAnimationFrame(bucle);

  globalThis[CLAVE] = () => {
    cancelAnimationFrame(id); caja.remove();
    if (muestras.length) console.log('fps medio:', Math.round(muestras.reduce((a, b) => a + b) / muestras.length), 'peor:', peor);
  };
  console.log('medidor de fps activo; vuelve a ejecutar para pararlo');
})();
Estos instrumentos miden lo que el panel de rendimiento mide, y sirven para otra cosa

Conviene ser preciso sobre qué hacen estos snippets y qué no, porque la tentación de sustituir el panel de Performance con ellos lleva a conclusiones malas. Los datos que consultan son exactamente los mismos que el panel usa —vienen de las mismas APIs del navegador— así que no son una aproximación ni una versión inferior. La diferencia no está en la precisión sino en la resolución y en el contexto. Un observador de tareas largas te dice que hubo una tarea de 340 milisegundos; el panel de rendimiento te dice qué funciones se ejecutaron dentro de ella, en qué proporción, y qué las llamó. Esa segunda información es la que te permite arreglar algo, y no hay forma de sacarla desde la consola. Entonces, ¿para qué sirven estos instrumentos? Para lo que el panel hace mal, que es la vigilancia continua. Grabar un perfil exige decidir cuándo grabar, y la mayoría de los problemas de rendimiento que sufren los usuarios ocurren en momentos que tú no estás grabando: al décimo minuto de uso, con la tercera pestaña abierta, después de cargar cuatrocientos elementos en una lista. Un observador instalado con un snippet vigila indefinidamente sin coste apreciable y sin generar gigabytes de traza, y cuando algo pasa, te lo cuenta con el instante exacto. La forma de trabajar que sale de combinarlos es la buena: usa los snippets para detectar y localizar en el tiempo, y el panel para diagnosticar el caso concreto que has detectado. Instala el observador, usa la aplicación con normalidad, y cuando el observador cante, ya sabes qué acción reproducir y en qué momento pulsar el botón de grabar. Eso convierte un perfil de treinta segundos ilegible en uno de dos segundos que apunta directamente al problema.