wandres.dev
LA RED · Latencia, ancho de banda y el RTT

El arranque lento de TCP y los primeros 14 KB

Por qué una conexión nueva no usa el ancho de banda disponible desde el principio, cuántos bytes caben en el primer envío, y qué consecuencias tiene para el diseño del documento.

⏱ 17 min

Una conexión TCP recién abierta no sabe cuánto puede enviar sin saturar la red, así que empieza con mucha prudencia y va probando. Ese mecanismo, el arranque lento, significa que el ancho de banda contratado no está disponible en los primeros milisegundos de una conexión, y explica una de las cifras más citadas y peor entendidas del rendimiento web: los primeros catorce kilobytes.

🎯 Al terminar esta lección sabrás
  • Explicar por qué existe el arranque lento y qué problema resuelve.
  • Calcular cuántos bytes caben en cada ronda de ida y vuelta de una conexión nueva.
  • Derivar de dónde sale la cifra de los 14 KB y qué la modifica.
  • Aplicar la consecuencia al tamaño y al contenido del documento HTML.

Por qué existe

TCP tiene que resolver un problema difícil: no sabe nada de la red por la que va a enviar. Podría ser un cable local de diez gigabits o una conexión móvil en un tren. Si envía demasiado rápido, satura los buffers intermedios, se pierden paquetes y el rendimiento se desploma. Si envía demasiado despacio, desaprovecha la capacidad.

La solución es empírica: empezar con poco y doblar la apuesta mientras las cosas vayan bien. El emisor mantiene una ventana de congestión, que es el número máximo de segmentos que puede tener en vuelo sin confirmar. Empieza pequeña y crece exponencialmente con cada ronda de confirmaciones, hasta que se pierde un paquete o se alcanza un umbral, momento en el que el crecimiento pasa a ser lineal.

Ese periodo de crecimiento exponencial inicial es el arranque lento, y el nombre es un poco engañoso: el crecimiento es rápido, lo lento es el punto de partida.

Cuánto cabe en cada ronda

El valor inicial de la ventana de congestión está estandarizado en 10 segmentos. Antes fueron 2, luego 4; se subió a 10 tras las mediciones que mostraron que era un valor seguro y notablemente mejor para el tráfico web, que se caracteriza por transferencias cortas.

El tamaño máximo de segmento habitual en internet es de 1.460 bytes de carga útil, resultado de restar las cabeceras de TCP y de IP a los 1.500 bytes de la unidad máxima de transmisión de Ethernet.

De ahí sale la cifra:

10 segmentos x 1.460 bytes = 14.600 bytes = 14,26 KiB

Unos 14 KB. Eso es lo que el servidor puede enviar en la primera ronda, antes de recibir ninguna confirmación. Si tu respuesta cabe ahí, llega completa en un solo viaje de ida y vuelta. Si no cabe, hay que esperar a que lleguen las confirmaciones para poder enviar más.

La progresión, si no se pierde nada, es esta:

Ronda Ventana Bytes acumulados RTT acumulados
1 10 segmentos ~14 KB 1
2 20 segmentos ~43 KB 2
3 40 segmentos ~100 KB 3
4 80 segmentos ~215 KB 4

Es exponencial, así que se recupera deprisa. Pero fíjate en el impacto para un documento HTML típico: un HTML de 100 KB comprimidos necesita tres rondas solo de transferencia, y esas tres rondas son 300 milisegundos con un RTT de 100. Un HTML de 14 KB necesita una.

ℹ️
Los 14 KB no son un número mágico, son un cálculo con supuestos

La cifra depende de la ventana inicial de tu servidor, que es configurable y que algunos sistemas y CDN suben, y del tamaño de segmento efectivo, que baja si hay un túnel o una VPN de por medio porque las cabeceras adicionales recortan la carga útil. Trátala como un orden de magnitud, no como un umbral exacto. Lo que no cambia es la estructura: hay un primer envío acotado y todo lo que exceda cuesta viajes de ida y vuelta adicionales.

Qué agrava el arranque lento

La reutilización de conexión no reinicia la ventana… salvo que sí. Una conexión que ya ha estado transfiriendo tiene su ventana crecida, así que las peticiones siguientes se benefician. Pero muchas implementaciones reducen la ventana tras un periodo de inactividad, precisamente porque las condiciones de la red pueden haber cambiado. Una conexión que estuvo parada varios segundos puede volver casi al principio.

La pérdida de paquetes es devastadora en esta fase. Perder un paquete durante el arranque lento hace que el emisor reduzca drásticamente la ventana. En redes móviles con pérdida no despreciable, esto es habitual y explica buena parte de la diferencia entre el rendimiento teórico y el real de una conexión móvil.

Cada conexión tiene su propia ventana. Abrir seis conexiones al mismo origen, como hacía HTTP/1.1, significa seis arranques lentos independientes, cada uno partiendo de diez segmentos. Es una de las razones por las que HTTP/2, con una sola conexión multiplexada, aprovecha mejor la red: hay un solo arranque lento y la ventana crece para todo el tráfico.

Las consecuencias prácticas

Primera: el tamaño del documento HTML importa más de lo que sugiere su peso. No porque 100 KB sean muchos bytes, sino porque no caben en la primera ronda. Reducir el HTML de 60 a 14 KB comprimidos ahorra un viaje de ida y vuelta completo, que en móvil son 100 a 200 milisegundos, y no hay optimización de bytes que rinda tanto por kilobyte eliminado.

Segunda: lo que metes en esos primeros 14 KB decide qué puede empezar el navegador. Si en el primer envío caben las referencias a tus recursos críticos, el escáner de precarga puede empezar a pedirlos mientras el resto del documento aún viaja. Si tus etiquetas <link> de CSS y tu <img> del hero están en el kilobyte 40, el navegador no sabe que existen hasta la tercera ronda.

Esto convierte el orden del <head> en una decisión de rendimiento con efecto medible, no en una cuestión de estilo:

<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Primero, lo que abre conexiones a origenes inevitables. -->
  <link rel="preconnect" href="https://api.ejemplo.com" crossorigin>

  <!-- Despues, lo que bloquea el renderizado. -->
  <link rel="stylesheet" href="/css/critico.css">

  <!-- Y lo que el escaner debe descubrir cuanto antes. -->
  <link rel="preload" as="image" href="/img/hero.avif" type="image/avif" fetchpriority="high">

  <title>Titulo de la pagina</title>
</head>

Tercera: inlinear tiene un límite duro. Meter el CSS crítico dentro del HTML ahorra una petición, pero si al hacerlo el documento pasa de 12 a 40 KB, has cambiado una petición por dos viajes de ida y vuelta adicionales de transferencia. El cálculo hay que hacerlo: inlinear compensa mientras el documento siga cupiendo, aproximadamente, en el primer envío.

Cuarta: la respuesta en streaming cambia el juego. Si tu servidor puede empezar a enviar el HTML antes de tenerlo completo, el navegador recibe y parsea la cabecera mientras el servidor sigue generando el cuerpo. Eso hace que el contenido del primer envío sea todavía más determinante, porque llega mientras el resto se está construyendo.

Cómo comprobarlo

La forma directa de ver el efecto es medir el tiempo entre el primer byte y el último de tu documento y compararlo con lo que predice el ancho de banda:

const nav = performance.getEntriesByType('navigation')[0];
const transferencia = nav.responseEnd - nav.responseStart;
const bytes = nav.encodedBodySize;   // bytes comprimidos recibidos

console.log({
  bytesComprimidos: bytes,
  bytesSinComprimir: nav.decodedBodySize,
  msDeTransferencia: Math.round(transferencia),
  MbpsEfectivos: ((bytes * 8) / (transferencia / 1000) / 1e6).toFixed(2),
});

Si el caudal efectivo que sale es mucho menor que el ancho de banda real de la conexión, estás viendo el arranque lento en acción: la conexión no llegó a alcanzar velocidad de crucero porque la transferencia terminó antes.

encodedBodySize y decodedBodySize son además la forma más rápida de verificar que la compresión está activa: si coinciden, tu servidor no está comprimiendo.

El arranque lento es la razón real por la que la primera visita a un sitio con muchos dominios va tan mal, y no es la que todo el mundo dice

La explicación habitual de por qué añadir orígenes es caro se queda en el coste de establecimiento: DNS, TCP y TLS, tres o cuatro viajes. Es cierto y es la mitad de la historia. La otra mitad es que cada conexión nueva arranca con su propia ventana de congestión mínima, así que aunque hayas pagado el establecimiento, esa conexión no puede usar tu ancho de banda: puede enviar catorce kilobytes y esperar. Cinco orígenes distintos son cinco conexiones que se reparten la red arrancando todas desde cero, compitiendo entre sí, y ninguna alcanza velocidad de crucero antes de terminar su trabajo. El efecto es especialmente cruel porque es invisible en la cascada: ves cinco recursos pequeños que tardan más de lo que su tamaño justifica, sin ninguna barra larga que señalar. Cuando consolidas esos cinco orígenes en uno, no solo te ahorras cuatro cadenas de establecimiento, sino que todo el tráfico pasa por una conexión cuya ventana ya ha crecido, y el efecto conjunto es bastante mayor que la suma de los establecimientos ahorrados. Es la razón por la que consolidar orígenes rinde más de lo que predice el cálculo ingenuo.