HTTP/2: multiplexación y el límite que no resuelve
El marco binario, los flujos, la compresión de cabeceras y la priorización, con el análisis honesto de qué mejora de verdad y qué sigue igual.
HTTP/2 no cambió la semántica de HTTP: los métodos, los códigos de estado y las cabeceras son los mismos. Lo que cambió es cómo se codifican y cómo se transportan, y ese cambio elimina el bloqueo de cabecera de línea en la capa de aplicación. Lo que no elimina, porque no puede, es el que ocurre por debajo. Esa frontera es el contenido de esta lección.
- Describir el marco binario y el concepto de flujo dentro de una conexión.
- Explicar cómo la compresión de cabeceras reduce el coste de las peticiones repetidas.
- Enunciar por qué el bloqueo persiste en la capa de transporte y cuándo se nota.
- Evaluar el estado real de la priorización y del envío desde el servidor.
El marco binario y los flujos
HTTP/1.1 es texto: las peticiones y respuestas son líneas legibles separadas por saltos de línea. Eso es cómodo para depurar y muy incómodo para multiplexar, porque no hay forma de saber dónde acaba un mensaje y empieza otro sin leerlo entero.
HTTP/2 sustituye el texto por un marco binario. Toda la comunicación se divide en tramas, cada una con una cabecera de tamaño fijo que indica su longitud, su tipo y a qué flujo pertenece.
Un flujo es un intercambio bidireccional de tramas dentro de una conexión, identificado por un número. Cada petición y su respuesta ocupan un flujo. Como cada trama lleva su identificador, las tramas de distintos flujos pueden intercalarse libremente en la conexión y el receptor las reordena.
Eso es la multiplexación, y su consecuencia inmediata es que el bloqueo de cabecera de línea de la capa de aplicación desaparece: una respuesta lenta ya no impide que otras avancen, porque sus tramas viajan intercaladas.
El número de flujos simultáneos lo negocia cada extremo con un parámetro de configuración. La especificación recomienda que no sea inferior a 100, y ese suele ser el valor que anuncian los servidores. En la práctica significa que no hay límite efectivo de peticiones en vuelo para una página normal.
La compresión de cabeceras
Un detalle que suele infravalorarse. Las cabeceras HTTP son muy repetitivas: el mismo agente de usuario, las mismas cookies, los mismos Accept en cada petición. En una página con doscientas peticiones, eso puede ser más tráfico que algunos recursos.
HTTP/2 comprime las cabeceras con un esquema que mantiene una tabla dinámica sincronizada entre cliente y servidor. La primera vez que aparece una cabecera se envía completa y se indexa; las siguientes veces se envía solo su índice. Un par de bytes en lugar de doscientos.
El efecto es más notable de lo que parece en sitios con cookies grandes: una cookie de sesión de un kilobyte enviada en cien peticiones son cien kilobytes por HTTP/1.1 y prácticamente nada por HTTP/2.
Hay una consecuencia operativa que conviene conocer: la tabla dinámica introduce un estado compartido, y eso hace que el orden y la repetición de las cabeceras afecten al tamaño. No es algo que tengas que optimizar, pero explica por qué el tamaño de las cabeceras que reporta una herramienta puede no cuadrar con la suma de sus caracteres.
El bloqueo que persiste
Aquí está el límite y la parte que más se simplifica al contarla.
HTTP/2 multiplexa sobre una única conexión TCP. Y TCP tiene una garantía fundamental: entrega los bytes al nivel superior estrictamente en orden. Si se pierde un segmento, el sistema operativo retiene todo lo que llegue después hasta que la retransmisión complete el hueco.
El problema es que TCP no sabe nada de flujos. Para él, la conexión es un único chorro de bytes. Cuando retiene datos por una pérdida, retiene bytes que pertenecen a flujos completamente distintos y que podrían entregarse ya. El bloqueo de cabecera de línea no ha desaparecido: se ha mudado de la capa de aplicación a la de transporte.
La consecuencia práctica es incómoda: en redes con pérdida de paquetes, HTTP/2 puede rendir peor que HTTP/1.1. Con seis conexiones, una pérdida bloquea aproximadamente un sexto del tráfico. Con una sola conexión multiplexada, lo bloquea todo.
Esto no es un caso teórico. Las redes móviles tienen pérdida no despreciable, y es exactamente la situación de una parte considerable de los usuarios.
Como todo el tráfico va por una conexión, esa conexión es un punto único de fallo de rendimiento. Merece la pena cuidarla: tiempos de espera de inactividad generosos en el servidor para no forzar reconexiones, y evitar cualquier cosa que fragmente el tráfico en varias conexiones, empezando por el sharding de dominios.
Priorización y envío desde el servidor
La priorización
HTTP/2 incluía un esquema de priorización basado en un árbol de dependencias con pesos, en el que el cliente podía indicar qué flujos eran más importantes y cómo repartir el ancho de banda entre ellos.
En la práctica no funcionó. Era complejo de implementar correctamente, los navegadores lo usaban de formas distintas, muchos servidores lo ignoraban por completo, y algunos lo implementaban de manera que empeoraba las cosas. El esquema original quedó desaconsejado.
Existe un esquema de sustitución mucho más simple, basado en dos parámetros que viajan como una cabecera: una urgencia en una escala pequeña de enteros y un indicador de si el recurso se puede entregar de forma incremental. Es más fácil de implementar correctamente y de razonar sobre él, y sirve tanto para HTTP/2 como para HTTP/3.
Para el desarrollador de frontend, la conclusión operativa es que no puedes contar con que la priorización del protocolo te salve. Lo que sí funciona de forma fiable son las señales del lado del navegador: el orden en el documento, la prioridad implícita que el navegador asigna a cada tipo de recurso, y el atributo fetchpriority.
El envío desde el servidor
HTTP/2 introdujo la capacidad del servidor de enviar recursos que el cliente no había pedido todavía, anticipándose. La idea era eliminar el viaje de ida y vuelta del descubrimiento: si el servidor sabe que el HTML va a necesitar una hoja de estilos, la envía junto con el HTML.
Fracasó y se retiró de Chrome. Las razones, todas prácticas:
- El servidor no sabe qué tiene el cliente en caché. Enviar un recurso que el cliente ya tiene desperdicia ancho de banda justo en el momento más crítico de la carga.
- Compite con lo que sí hace falta. Los bytes empujados ocupan la conexión y retrasan el propio documento.
- Era difícil de usar correctamente. Las mediciones mostraban resultados mixtos incluso en implementaciones cuidadosas.
Su sustituto es una cabecera de respuesta con la que el servidor indica al cliente qué recursos va a necesitar, y el cliente decide si los pide. Cuesta un viaje de ida y vuelta más que empujarlos, pero respeta la caché y la decisión del cliente:
HTTP/2 200
content-type: text/html; charset=utf-8
link: </css/critico.css>; rel=preload; as=style
link: </img/hero.avif>; rel=preload; as=image; fetchpriority=high
Esa cabecera tiene una ventaja adicional: el navegador la ve antes de recibir una sola línea del cuerpo, así que puede empezar a descargar mientras el servidor aún está generando el HTML. En un backend con tiempo de proceso apreciable, eso es tiempo real ganado.
Qué esperar en la práctica
Un balance honesto, porque las expectativas suelen estar infladas.
| Situación | Efecto de HTTP/2 frente a HTTP/1.1 |
|---|---|
| Muchos recursos pequeños del mismo origen | Mejora clara |
| Cabeceras o cookies grandes | Mejora clara |
| Red con pérdida de paquetes apreciable | Puede empeorar |
| Pocos recursos grandes | Efecto pequeño |
| Sitio con sharding de dominios sin corregir | Se desperdicia la mejora |
| Latencia alta con pocas peticiones | Efecto pequeño; el problema es la latencia |
La conclusión es que HTTP/2 es una mejora sólida y no es transformadora por sí sola. Lo que sí transforma es combinarla con eliminar el sharding, consolidar orígenes y dejar de concatenarlo todo, porque esas decisiones dejan de tener sentido cuando la multiplexación funciona.
El caso que más veces he visto no es que HTTP/2 no ayude, es que las optimizaciones heredadas lo neutralizan y a veces lo invierten. Un sitio con recursos repartidos en cuatro subdominios sirve por HTTP/2 usando cuatro conexiones en lugar de una: paga cuatro establecimientos, cuatro arranques lentos, y pierde por completo la priorización, que solo opera dentro de una conexión. Otro sitio que concatena todo su JavaScript en un único archivo de trescientos kilobytes sigue invalidando el archivo entero cada vez que cambia una línea, cuando con multiplexación podría servir veinte módulos cacheados por separado y volver a descargar solo el que cambió. Y un tercero que incrusta sus imágenes pequeñas como URL de datos dentro del CSS está pagando esos bytes en cada carga, sin caché independiente y con coste de decodificación, para ahorrar unas peticiones que ya no cuestan nada. En los tres casos la migración de protocolo se hizo bien y el resultado medido fue plano o peor, y en los tres el diagnóstico fue el mismo: la ganancia estaba, pero la arquitectura de recursos seguía optimizada para un protocolo que ya no se usaba. La migración de verdad no es cambiar la configuración del servidor, es revisar las decisiones que se tomaron por culpa del límite de seis conexiones.