El coste real de un script de tercero: los cinco componentes
Por qué el peso es el componente menos importante de los cinco, cómo se multiplica el coste por las peticiones en cascada, y la aritmética que convierte un contenedor de veinte kilobytes en dos segundos de bloqueo.
La conversación sobre terceros casi siempre empieza y acaba en el mismo sitio: cuántos kilobytes pesa. Es el componente más fácil de medir y el que menos explica. Un contenedor de etiquetas de veinte kilobytes puede acabar costando dos segundos de hilo principal bloqueado y treinta peticiones a orígenes que nadie ha auditado, y ninguno de esos dos números aparece en la columna de tamaño. Esta lección desglosa los cinco costes reales y da la aritmética para sumarlos.
- Descomponer el coste de un tercero en sus cinco componentes y saber cuál domina.
- Calcular el coste de una cascada de peticiones a partir de su profundidad y del RTT.
- Estimar el coste de ejecución a partir del tamaño y del multiplicador del dispositivo objetivo.
- Reconocer los tres costes que no aparecen en ninguna herramienta de rendimiento.
Los cinco componentes
Uno: los bytes transferidos. El componente más visible y el menos determinante. Un script de cien kilobytes comprimidos en una conexión de dos megabits tarda unos 400 milisegundos en descargarse, y eso ocurre en paralelo con otras cosas. Es un coste real pero acotado y bien entendido.
Dos: el tiempo de ejecución en el hilo principal. Aquí está casi todo. Un byte de JavaScript no se descarga y ya está: se analiza, se compila y se ejecuta, y esas tres fases ocurren en el hilo principal, bloqueándolo. La relación aproximada entre tamaño y tiempo, con las salvedades de siempre, es del orden de un milisegundo por cada kilobyte de JavaScript en un dispositivo de gama media, y de dos a cuatro veces más en gama baja. Cien kilobytes de tercero son, por tanto, entre 100 y 400 milisegundos de hilo bloqueado, y eso antes de contar lo que el script haga después.
Tres: las peticiones en cascada. El componente que más se subestima y el que produce los peores números. Un contenedor de etiquetas no es un script: es un cargador de scripts. Descarga, se ejecuta, y entonces decide qué más pedir. Cada nivel de esa cascada es una ida y vuelta completa, y cada origen nuevo añade resolución de nombre, apretón de manos y negociación TLS.
Cuatro: el trabajo que el script provoca después. Observadores de mutaciones que reaccionan a cada cambio del DOM, temporizadores periódicos, escuchadores de eventos en document que se ejecutan en cada desplazamiento, inserción de iframes que arrancan un contexto de navegación completo con su propio hilo y su propia memoria. Este coste no se ve en el arranque; se ve en el INP y en el consumo de batería.
Cinco: los costes que no miden las herramientas. El riesgo de disponibilidad, porque un script de tercero en el camino crítico convierte su caída en la tuya; el riesgo de seguridad, porque ese código tiene acceso completo a tu DOM y a las cookies del usuario; y el coste de la variabilidad, porque el tercero puede desplegar cuando quiera y multiplicar por tres su peso sin avisarte.
La aritmética de la cascada
La cascada es donde está el daño y merece un cálculo explícito. Considera la estructura típica de un contenedor de etiquetas:
Nivel 0 documento HTML
Nivel 1 contenedor.js del gestor de etiquetas 20 KB
Nivel 2 analitica.js + 2 pixeles 45 KB
Nivel 3 modulo-remarketing.js 30 KB
Nivel 4 pixel de la red publicitaria 1 KB
Cuatro niveles. Cada uno solo puede empezar cuando el anterior ha terminado de descargarse y de ejecutarse, porque es la ejecución la que descubre la URL siguiente. El coste de un nivel es:
coste de un nivel = resolucion de nombre
+ apretones de mano si el origen es nuevo
+ ida y vuelta de la peticion
+ descarga
+ ejecucion en el hilo principal
Con un origen nuevo, un RTT de 80 milisegundos y TLS 1.3, el establecimiento son unas tres idas y vueltas: 240 milisegundos antes del primer byte. Añade la descarga y la ejecución y un nivel cuesta fácilmente entre 300 y 500 milisegundos.
4 niveles x 350 ms de media = 1.400 ms
Mil cuatrocientos milisegundos, de los cuales apenas noventa y seis kilobytes de bytes explican una fracción minúscula. La profundidad de la cascada domina sobre el tamaño total en cuanto hay más de dos niveles. Es la aplicación directa de la asimetría entre latencia y ancho de banda que vimos en la geografía de la latencia: un recurso que solo puede descubrirse ejecutando otro multiplica el recurso que no mejora con el tiempo.
De aquí salen dos conclusiones operativas que valen más que cualquier optimización posterior. La primera: aplanar la cascada vale más que reducir su tamaño. Cargar directamente los tres scripts que de verdad usas, en lugar del contenedor que los descubre, elimina dos niveles enteros. La segunda: cada origen distinto tiene un coste fijo de establecimiento, así que ocho terceros en ocho dominios cuestan mucho más que ocho scripts del mismo dominio. preconnect a los orígenes críticos ayuda, pero tiene su propio límite y no es gratis.
Del tamaño al tiempo de bloqueo
Para poder comparar terceros entre sí hace falta una estimación rápida del coste en hilo principal que no requiera montar un experimento. Esta sirve como primera aproximación:
// Coste aproximado en hilo principal a partir del Resource Timing.
// El multiplicador se calibra midiendo un script conocido en el
// dispositivo objetivo; 1 es un portatil, 4 un movil de gama media,
// 8 o mas gama baja.
function costeAproximado(multiplicador = 4) {
const porOrigen = new Map();
for (const r of performance.getEntriesByType('resource')) {
if (r.initiatorType !== 'script' && !r.name.endsWith('.js')) continue;
const origen = new URL(r.name).origin;
if (origen === location.origin) continue;
const kb = (r.decodedBodySize || r.transferSize || 0) / 1024;
const actual = porOrigen.get(origen) || { kb: 0, peticiones: 0 };
actual.kb += kb;
actual.peticiones += 1;
porOrigen.set(origen, actual);
}
return [...porOrigen.entries()]
.map(([origen, d]) => ({
origen,
peticiones: d.peticiones,
kb: Math.round(d.kb),
// 1 ms por KB de JS en gama media es el orden de magnitud.
msEstimados: Math.round(d.kb * multiplicador),
}))
.sort((a, b) => b.msEstimados - a.msEstimados);
}
console.table(costeAproximado(4));
Es una estimación, no una medida, y hay que decirlo cada vez que se enseñe el resultado. Sirve para ordenar candidatos y para tener una cifra en la conversación inicial; el número real se obtiene con la atribución por script de la lección siguiente. Dos avisos sobre el código: decodedBodySize es el tamaño descomprimido, que es el que importa para el tiempo de análisis, y viene a cero para orígenes que no envían Timing-Allow-Origin, cosa muy común precisamente entre los terceros. Cuando veas ceros, no es que sean gratis.
Los órdenes de magnitud habituales, para tener una referencia de partida. Los tuyos serán distintos y hay que medirlos, pero si tu medición se sale un orden de magnitud de esta tabla, revisa la medición:
| Categoría | Bytes típicos | Bloqueo típico en gama media | Peticiones |
|---|---|---|---|
| Contenedor de etiquetas vacío | 20–40 KB | 50–150 ms | 1 |
| Contenedor con seis etiquetas | 150–400 KB | 400–900 ms | 15–40 |
| Analítica de producto | 40–90 KB | 100–300 ms | 2–5 |
| Grabación de sesión | 100–250 KB | 300–800 ms | continuo |
| Chat de soporte | 200–600 KB | 400–1.200 ms | 5–15 |
| Incrustación de vídeo | 800 KB–1,5 MB | 500–1.500 ms | 20–60 |
| Mapa interactivo | 300–900 KB | 400–1.000 ms | 10–40 |
| Red publicitaria | muy variable | 500 ms–varios segundos | 20–100 |
La grabación de sesión merece una nota aparte porque su perfil es distinto de todos los demás: su coste no está en el arranque sino repartido durante toda la visita, en forma de observador de mutaciones que serializa cambios del DOM continuamente. En una interfaz que cambia mucho, es una de las cosas que más daño hacen al INP, y es de las que menos aparecen en un análisis de carga porque el análisis de carga termina antes de que el usuario haga nada.
Los tres costes invisibles
Disponibilidad. Un script de tercero cargado de forma bloqueante en el head convierte su caída en la tuya. Y no hace falta que caiga: basta con que responda lento. Un script síncrono de un origen que tarda diez segundos en responder es una página en blanco durante diez segundos. La defensa mínima es que ningún tercero sea síncrono ni bloqueante, y la buena es un tiempo límite explícito:
<!-- Nunca asi: bloquea el analisis del documento -->
<script src="https://tercero.ejemplo/tag.js"></script>
<!-- Asi: no bloquea, y si falla la pagina sigue -->
<script src="https://tercero.ejemplo/tag.js" async
onerror="console.warn('tercero no disponible')"></script>
Seguridad. Un script de tercero se ejecuta con todos los privilegios de tu origen: lee tu DOM, tus cookies no marcadas como HttpOnly, el contenido de tus formularios. Una cadena de cuatro niveles significa que estás confiando también en los proveedores de tu proveedor, a los que no has evaluado. La herramienta que acota esto es una política de seguridad de contenido con lista de orígenes permitidos, y Subresource Integrity para los scripts cuyo contenido no debería cambiar. Ninguna de las dos es de rendimiento, pero la lista de orígenes permitidos tiene un efecto lateral valioso: hace visible la cascada, porque cada nivel nuevo aparece como una violación en el informe.
Variabilidad. Tú controlas cuándo cambia tu paquete; no controlas cuándo cambia el suyo. Un tercero puede triplicar su tamaño en un despliegue del que no te enteras. Por eso las medidas de terceros no se hacen una vez: se vigilan, y ese es uno de los pocos casos en los que una alerta automática sobre un número de campo está totalmente justificada.
Merece la pena detenerse en la asimetría de esta relación, porque explica por qué las conversaciones sobre terceros son tan improductivas cuando se plantean como una discusión técnica. Cuando añades una dependencia de tu paquete, tú decides la versión, tú decides cuándo actualizar, el código pasa por tu proceso de compilación, tu análisis estático y tu revisión, y si la nueva versión pesa el triple lo ves en el diff antes de aprobarlo. Nada de eso ocurre con una etiqueta de terceros. La URL es fija y el contenido detrás de ella cambia cuando el proveedor quiera, sin número de versión, sin nota de publicación, sin que tú lo apruebes y sin que te enteres. Has firmado, en la práctica, un despliegue continuo de código ajeno en los navegadores de tus usuarios, con permisos totales sobre tu origen y con tu marca en la barra de direcciones cuando algo salga mal. Y la asimetría se extiende a los incentivos, que es donde está la parte incómoda: el proveedor no tiene ningún motivo para optimizar su script, porque el coste lo pagas tú en tus métricas y él lo cobra en su factura. Cada kilobyte que añade le sirve para vender una funcionalidad más y no le cuesta nada. Interiorizar esto cambia dos cosas en la práctica. La primera es la instrumentación: si el contenido puede cambiar sin aviso, medirlo una vez no sirve de nada, y lo que necesitas es una vigilancia continua del peso y del tiempo de ejecución por origen, con una alerta cuando se salga de su rango histórico. Es el único caso de todo este track en el que defiendo poner una alerta sobre un número de campo con un umbral, precisamente porque el cambio que buscas no es una deriva gradual sino un salto brusco, que es lo que las alertas detectan bien. La segunda es contractual antes que técnica: si un tercero es lo bastante importante como para estar en el camino crítico, su comportamiento debería estar en el contrato, con un compromiso de peso máximo y de disponibilidad, y quien firma ese contrato tiene que saber que existe esa cláusula. Suena a burocracia y es la única palanca real que tienes: el equipo de ingeniería no puede negociar con el proveedor, pero quien paga la factura sí. La consecuencia final es que el trabajo de rendimiento con terceros no es sobre todo técnico: es sobre todo de gobernanza, y quien intenta resolverlo solo con carga diferida y trucos de red acaba peleando cada trimestre la misma batalla con un tercero distinto.
- Ejecuta el estimador de arriba en tus cinco páginas más visitadas y ordena los orígenes por milisegundos estimados.
- Dibuja la cascada de tu contenedor de etiquetas con la columna de iniciador de las herramientas de red. Cuenta los niveles.
- Cuenta cuántos orígenes distintos participan en tu carga y multiplica por tres idas y vueltas de establecimiento.
- Busca en tu HTML cualquier script de tercero sin
asyncnidefer. Cada uno es una página en blanco esperando a un servidor ajeno. - Guarda hoy el peso descomprimido de tus cinco terceros principales y repite la medida dentro de un mes.