HTTP/2 prometió una web mucho más rápida y Cloudflare lanzó el acceso HTTP/2 para todos nuestros clientes hace mucho, mucho tiempo. Pero una característica de HTTP/2, la priorización, no cumplió con las expectativas. No porque estuviera fundamentalmente dañada, sino por la forma en que los navegadores la implementaron.
Actualmente, Cloudflare está impulsando un cambio en la priorización de HTTP/2 que le otorga a nuestros servidores el control de las decisiones de priorización que realmente hacen que la web sea mucho más rápida.
Tradicionalmente, el navegador ha tenido el poder de decidir cómo y cuándo se carga el contenido web. Hoy, estamos introduciendo un cambio radical en ese modelo para todos los planes pagos que deja el control directamente en manos del propietario. Los clientes pueden habilitar “Priorización de HTTP/2 optimizada” en la pestaña Speed (Velocidad) del panel de control de Cloudflare: esto anula los valores predeterminados del navegador con un esquema de programación mejorado que da como resultado una experiencia de visitante significativamente más rápida (hemos visto un aumento de la velocidad del 50% en múltiples ocasiones). Con Cloudflare Workers, los propietarios de sitios pueden dar un paso más y personalizar completamente la experiencia según sus necesidades específicas.
Antecedentes
Las páginas web se componen de docenas (a veces cientos) de recursos independientes que el navegador carga y reúne en el contenido final que se muestra. Esto incluye el contenido visible con el que el usuario interactúa (HTML, CSS, imágenes), así como la lógica de la aplicación (JavaScript) para el propio sitio, anuncios, análisis para el seguimiento del uso del sitio y beacons (sensores) de seguimiento de marketing. La secuencia de cómo se cargan esos recursos puede tener un impacto significativo en el tiempo que tarda el usuario en ver el contenido e interactuar con la página.
Un navegador es básicamente un motor de procesamiento HTML que pasa a través del documento HTML y sigue las instrucciones en orden desde el inicio hasta el final del HTML, creando la página a medida que avanza. Las referencias a las hojas de estilo (CSS) indican al navegador cómo aplicar estilo al contenido de la página, y el navegador retrasará la visualización del contenido hasta que haya cargado la hoja de estilos (para saber cómo aplicar estilo al contenido que va a mostrar). Los scripts a los que se hace referencia en el documento pueden tener varios comportamientos diferentes. Si el script está etiquetado como “async” o “defer”, el navegador puede seguir procesando el documento y simplemente ejecutar el código de script cuando los scripts estén disponibles. Si los scripts no están etiquetados como async o defer, el navegador DEBE detener el procesamiento del documento hasta que el script se haya descargado y ejecutado antes de continuar. Estos se conocen como scripts de “bloqueo”, porque impiden que el navegador continúe procesando el documento hasta que este se haya cargado y ejecutado.
El documento HTML se divide en dos partes. El (título) del documento está al comienzo y contiene hojas de estilo, scripts y otras instrucciones que el navegador necesita para mostrar el contenido. El (cuerpo) del documento está después del título y presenta el contenido real de la página que se muestra en la ventana del navegador (aunque los scripts y las hojas de estilo también pueden estar en el cuerpo). Hasta que el navegador llegue al cuerpo del documento no hay nada que mostrar al usuario, y la página permanecerá en blanco, por lo tanto, es importante pasar por el título del documento lo más rápido posible. “HTML5 rocks” tiene un excelente tutorial sobre cómo funcionan los navegadores si quiere obtener más detalles.
Por lo general, el navegador determina el orden de carga de los diferentes recursos que necesita para crear la página y continuar procesando el documento. En el caso de HTTP/1.x, el navegador está limitado por la cantidad de cosas que puede solicitar a la vez de cualquier servidor (generalmente 6 conexiones y solo un recurso a la vez por conexión), por lo tanto, el orden es estrictamente controlado por el navegador por la forma en que se solicitan las cosas. Con HTTP/2 las cosas cambian de manera significativa. El navegador puede solicitar todos los recursos a la vez (al menos tan pronto los conozca) y brinda instrucciones detalladas al servidor sobre la manera en que se deben entregar los recursos.
Orden óptimo de los recursos
Para la mayoría de las partes del ciclo de carga de la página hay un orden óptimo de los recursos que generará una experiencia de usuario más rápida (y la diferencia entre óptimo y no óptimo puede ser significativa y representar una mejora del 50% o más).
Como se describió anteriormente, al principio del ciclo de carga de la página, antes de que el navegador pueda representar cualquier contenido se bloquea en el CSS y bloquea JavaScript en la sección del HTML. Durante esa parte del ciclo de carga es mejor que el 100% del ancho de banda de conexión se utilice para descargar los recursos de bloqueo y que se descarguen uno a la vez en el orden en que se definen en el HTML. Esto permite al navegador analizar y ejecutar cada elemento mientras descarga el siguiente recurso de bloqueo, lo que permite que se canalice la descarga y la ejecución.
Los scripts tardan la misma cantidad de tiempo en descargarse cuando lo hacen en paralelo o uno detrás de otro, pero al descargarlos en secuencia, el primer script se puede procesar y ejecutar mientras se descarga el segundo script.
Una vez que el contenido de bloqueo de la representación se ha cargado las cosas se vuelven un poco más interesantes y la carga óptima puede depender del sitio específico o incluso de las prioridades comerciales (contenido del usuario vs. anuncios vs. análisis, etc.). Un tipo de letra en particular puede resultar difícil, ya que el navegador solo descubre qué tipos de letra necesita después de que las hojas de estilo se han aplicado al contenido que está a punto de mostrarse, de manera que al momento en que el navegador toma conocimiento de un tipo de letra, es necesario mostrar el texto que ya está listo para presentar en la pantalla. Cualquier retraso en la carga del tipo de letra termina con puntos y texto en blanco en la pantalla (o se muestra texto con el tipo de letra incorrecta).
Por lo general, se deben considerar ciertas compensaciones:
Los tipos de letras personalizados y las imágenes visibles en la parte de la página que se puede visualizar (ventana gráfica) se deben cargar lo más rápido posible. Estos afectan directamente la experiencia visual del usuario de la carga de la página.
JavaScript sin bloqueo debe descargarse en serie en relación con otros recursos de JavaScript para que la ejecución de cada uno se pueda canalizar con las descargas. JavaScript puede incluir una lógica de aplicación orientada al usuario, así como seguimiento de análisis y beacons (sensores) de marketing. La demora puede causar una caída en las métricas de las que la empresa hace un seguimiento.
Las imágenes se benefician de la descarga en paralelo. Los primeros bytes de un archivo de imagen contienen las dimensiones de imagen que pueden ser necesarias para el diseño del navegador, y la descarga progresiva de imágenes en paralelo puede visualizarse completa una vez que se han transferido alrededor del 50% de los bytes.
Al ponderar las compensaciones, una estrategia que funciona bien en la mayoría de los casos es la siguiente:
Los tipos de letras personalizadas se descargan en secuencia y dividen el ancho de banda disponible con imágenes visibles.
Las imágenes visibles se descargan en paralelo, dividiendo la parte de las “imágenes” del ancho de banda entre ellas.
Cuando no hay más tipos de letras o imágenes visibles pendientes:
Los scripts sin bloqueo se descargan en secuencia y dividen el ancho de banda disponible con imágenes no visibles
Las imágenes no visibles se descargan en paralelo, dividiendo la parte de las “imágenes” del ancho de banda entre ellas.
De esta manera, el contenido visible para el usuario se carga lo más rápido posible, la lógica de la aplicación se retrasa lo menos posible y las imágenes no visibles se cargan de tal manera que el diseño se complete lo más rápido posible.
Ejemple
A fines ilustrativos, utilizaremos una página de categoría de producto simplificada de un sitio de comercio electrónico típico. En este ejemplo la página tiene:
El archivo HTML de la propia página, representado por un casillero azul.
1 hoja de estilo externa (archivo CSS), representada por un casillero verde.
4 scripts externos (JavaScript), representados por casilleros naranjas. 2 de los scripts se bloquean al principio de la página y 2 son asincrónicos. Los casilleros de script de bloqueo utilizan un tono naranja más oscuro.
1 tipo de letra web personalizada, representada por un casillero rojo.
13 imágenes, representadas por casilleros púrpura. El logotipo de la página y 4 de las imágenes del producto son visibles en la ventana gráfica y 8 de las imágenes del producto exigen desplazamiento para visualizarse. Las 5 imágenes visibles utilizan un tono púrpura más oscuro.
Por cuestiones de simplicidad, supondremos que todos los recursos son del mismo tamaño y que cada uno tarda 1 segundo en descargarse en la conexión del visitante. La carga total demora 20 segundos, pero CÓMO se carga puede afectar en gran medida la experiencia.
Esta es la apariencia que tendría la carga óptima descripta en el navegador a medida que se cargan los recursos:
La página está en blanco durante los primeros 4 segundos mientras se cargan los scripts HTML, CSS y de bloqueo. Todos usan el 100% de la conexión.
A los 4 segundos, el fondo y la estructura de la página se muestran sin texto ni imágenes.
Un segundo más tarde, a los 5 segundos, se muestra el texto de la página.
Las imágenes tardan de 5 a 10 segundos en cargarse, al principio aparecen borrosas pero rápidamente adquieren nitidez. Alrededor de los 7 segundos, la apariencia de la página es casi idéntica a la versión final.
A los 10 segundos, todo el contenido visual de la ventana gráfica se ha terminado de cargar.
Durante los próximos 2 segundos, el JavaScript asincrónico se carga y ejecuta, procesando cualquier lógica no crítica (análisis, etiquetas de marketing, etc.).
Durante los últimos 8 segundos, se cargan el resto de las imágenes del producto para que estén listas cuando el usuario se desplace.
Priorización del navegador actual
Todos los motores de navegación implementan diferentes estrategias de priorización, ninguna de las cuales es óptima.
Microsoft Edge e Internet Explorer no admiten la priorización, por lo tanto, todo recae en el HTTP/2 predeterminado, que consiste en cargar todo en paralelo, dividiendo el ancho de banda de forma uniforme entre todo. Microsoft Edge está comenzando a utilizar el motor del navegador Chromium en futuras versiones de Windows, lo que ayudará a mejorar la situación. En nuestra página de ejemplo esto significa que el navegador está atascado en el título durante la mayor parte del tiempo de la carga, ya que las imágenes están ralentizando la transferencia de los scripts de bloqueo y de las hojas de estilo.
Visualmente, esto da como resultado una experiencia bastante desagradable al tener que mirar una pantalla en blanco durante 19 segundos antes de que se muestre la mayor parte del contenido, y luego una demora de 1 segundo para que se muestre el texto. Tenga paciencia al ver el progreso animado, porque durante los 19 segundos de pantalla en blanco puede parecer que no sucede nada (pero en realidad no es así):
Safari carga todos los recursos en paralelo, dividiendo el ancho de banda entre ellos en función de la importancia que le asigna Safari (con recursos que bloquean la representación como scripts y hojas de estilo que son más importantes que las imágenes). Las imágenes se cargan en paralelo, pero también se cargan al mismo tiempo que el contenido que bloquea la representación.
Si bien es similar a Edge porque todo se descarga al mismo tiempo, al asignar más banda ancha a los recursos que bloquean la representación, Safari muestra el contenido mucho más rápido:
A los 8 segundos aproximadamente, la hoja de estilo y los scripts han terminado de cargarse para que la página pueda empezar a mostrarse. Como las imágenes se cargaban en paralelo, también se podían representar en su estado parcial (borroso para imágenes progresivas). Esto sigue siendo dos veces más lento que el caso óptimo, pero mucho mejor que lo que vimos con Edge.
En aproximadamente 11 segundos el tipo de letra se ha cargado, por lo tanto, se puede mostrar el texto y se habrán descargado más datos de imagen para que las imágenes sean un poco más nítidas. Esto se puede comparar con la experiencia de alrededor de 7 segundos para el caso de carga óptimo.
Durante los 9 segundos restantes de la carga, las imágenes adquieren más nitidez a medida que se descargan más datos, hasta que finalmente se completa a los 20 segundos.
Firefox crea un árbol de dependencias que agrupa los recursos y luego los programa para cargar uno tras otro o para compartir el ancho de banda entre los grupos. Dentro de un grupo determinado, los recursos comparten ancho de banda y se descargan simultáneamente. La carga de las imágenes se programa para después de la carga de las hojas de estilo que bloquean la representación y esta se hace en paralelo, pero los scripts y las hojas de estilo que bloquean la representación también se cargan en paralelo y no obtienen las ventajas de la canalización.
En nuestro ejemplo, esta experiencia es un poco más rápida que con Safari, ya que las imágenes se retrasan hasta que se completan las hojas de estilo:
A los 6 segundos, el contenido de la página de inicio se representa con el fondo y las imágenes borrosas del producto (en comparación con 8 segundos para Safari y 4 segundos para el caso óptimo).
A los 8 segundos, el tipo de letra se ha cargado y el texto se puede mostrar junto con versiones ligeramente más nítidas de las imágenes del producto (en comparación con 11 segundos para Safari y 7 segundos en el caso óptimo).
Durante los 12 segundos restantes de la carga, las imágenes del producto adquieren más nitidez a medida que se carga el contenido restante.
Chrome (y todos los navegadores que se basan en Chromium) prioriza los recursos en una lista. Esto funciona muy bien para el contenido que bloquea la representación que se beneficia de la carga en orden, pero funciona no funciona tan bien para las imágenes. Cada imagen se carga al 100% antes de iniciar la siguiente imagen.
En la práctica, esto es casi tan bueno como el caso de carga óptima, con la única diferencia de que las imágenes se cargan una por vez en lugar de hacerlo en paralelo:
Hasta los 5 segundos, la experiencia de Chrome es idéntica al caso óptimo, ya que muestra el fondo en 4 segundos y el contenido del texto en 5.
Durante los próximos 5 segundos, las imágenes visibles se cargan una por vez hasta que todas están completas a los 10 segundos (en comparación con el caso óptimo donde están ligeramente borrosas a los 7 segundos y adquieren nitidez en los 3 segundos restantes).
Una vez que se completa la parte visual de la página en 10 segundos (idéntica al caso óptimo), en los 10 segundos restantes se ejecutan los scripts asincrónicos y se cargan las imágenes ocultas (al igual que con el caso de carga óptimo).
Comparación visual
A nivel visual, el impacto puede ser enorme, a pesar de que todos tardan la misma cantidad de tiempo en cargar técnicamente todo el contenido:
Priorización del lado del servidor
El cliente (navegador) es quien solicita la priorización de HTTP/2 y el servidor decide qué hacer en función de la solicitud. Una buena cantidad de servidores no admiten hacer nada en absoluto con la priorización, pero para aquellos que lo hacen, todos respetan la solicitud del cliente. Otra opción sería decidir la mejor priorización para el lado del servidor, teniendo en cuenta la solicitud del cliente.
Según la especificación, la priorización de HTTP/2 es un árbol de dependencias que requiere un total conocimiento de todas las solicitudes en curso para poder priorizar los recursos entre sí. Si bien eso permite estrategias increíblemente complejas, es difícil de implementar bien en el navegador o del lado del servidor (como lo demuestran las diferentes estrategias del navegador y los diferentes niveles de soporte del servidor). Para facilitar la gestión de la priorización, hemos desarrollado un esquema de priorización más sencillo que tiene la flexibilidad necesaria para una programación óptima.
El esquema de priorización de Cloudflare tiene 64 “niveles” de prioridad, y dentro de cada uno de estos niveles hay grupos de recursos que determinan cómo se comparte la conexión entre ellos:
Todos los recursos en un nivel de prioridad superior se transfieren antes de pasar al nivel de prioridad inmediatamente inferior.
Dentro de un nivel de prioridad determinado, hay 3 grupos de “concurrencia” diferentes:
0 : Todos los recursos del grupo de concurrencia “0” se envían en secuencia en el orden en que se solicitaron, y utilizan el 100% del ancho de banda. Solo después de que se hayan descargado todos los recursos del grupo de concurrencia “0” se consideran otros grupos en el mismo nivel.
1 : Todos los recursos del grupo de concurrencia “1” se envían en secuencia en el orden en que se solicitaron. El ancho de banda disponible se divide de manera uniforme entre el grupo de concurrencia “1” y el grupo de concurrencia “n”.
N: Los recursos del grupo de concurrencia “n” se envían en paralelo, dividiendo entre ellos el ancho de banda disponible para el grupo.
En la práctica, el grupo de concurrencia “0” es útil para el contenido crítico que debe procesarse en secuencia (scripts, CSS, etc.). El grupo de concurrencia “1” es útil para contenido menos importante que puede compartir ancho de banda con otros recursos, pero donde los propios recursos aún se benefician del procesamiento en secuencia (scripts asincrónicos, imágenes no progresivas, etc.). El grupo de concurrencia “n” es útil para los recursos que se benefician del procesamiento en paralelo (imágenes progresivas, video, audio, etc.).
Priorización predeterminada de Cloudflare
Cuando está activada, la priorización optimizada implementa la programación “óptima” de los recursos que se describen anteriormente. Las priorizaciones específicas aplicadas se ven de la siguiente manera:
Este esquema de priorización permite enviar el contenido de bloqueo de representación en serie, seguido de las imágenes visibles en paralelo y, a continuación, el resto del contenido de la página con un nivel de uso compartido para equilibrar la carga de aplicaciones y contenido. La advertencia “* Si se detectó” implica que no todos los navegadores establecen la diferencia entre los tipos de hojas de estilo y scripts, pero seguirá siendo significativamente más rápido en todos los casos. El 50% más de velocidad predeterminada, especialmente para los visitantes de Edge y Safari, es bastante habitual:
Personalización de la priorización con Workers
La mayor velocidad predeterminada es excelente, pero lo interesante es que la capacidad para configurar la priorización también se expone a Cloudflare Workers, por lo tanto, los sitios pueden invalidar la priorización predeterminada de los recursos o implementar sus propios esquemas de priorización completos.
Si Worker agrega un encabezado “cf-priority” a la respuesta, los servidores perimetrales de Cloudflare usarán la prioridad y la concurrencia especificadas para esa respuesta. El formato del encabezado es /, por lo tanto, algo como response.headers.set('cf-priority', "30/0"); establecería la prioridad en 30 con una concurrencia de 0 para la respuesta determinada. De forma similar, “30/1” establecería la concurrencia en 1 y “30/n” establecería la concurrencia en n.
Con este nivel de flexibilidad, un sitio puede ajustar la priorización de recursos para satisfacer sus necesidades. Priorizar algunos scripts asincrónicos críticos, por ejemplo, o priorizar las imágenes de héroe antes que el navegador ha identificado que están en el área de visualización.
Para ayudar a informar las decisiones de priorización, el tiempo de ejecución de Workers también expone la información de priorización solicitada por el navegador en el objetivo de la solicitud transmitido a la escucha de eventos de búsqueda de Worker (request.cf.requestPriority). La prioridad solicitada entrante es una lista de atributos separada por punto y coma que tiene un aspecto similar al siguiente: “weight=192;exclusive=0;group=3;group-weight=127”.
Peso: el peso solicitado por el navegador para la priorización HTTP/2.
Exclusivo: la marca exclusiva HTTP/2 solicitada por el navegador (1 para navegadores basados en Chromium, 0 para otros).
Grupo: ID de secuencia HTTP/2 para el grupo de solicitudes (solo distinto de cero para Firefox).
Peso del grupo: peso HTTP/2 para el grupo de solicitudes (solo distinto de cero para Firefox).
Esto es solo el comienzo
La capacidad de ajustar y controlar la priorización de las respuestas es el componente básico del que se beneficiarán muchos trabajos en el futuro. Implementaremos nuestras propias optimizaciones avanzadas pero mediante la exposición en Workers también la hemos abierto a sitios e investigadores para experimentar con diferentes estrategias de priorización. Con el mercado de aplicaciones también es posible que las empresas desarrollen nuevos servicios de optimización sobre la plataforma Workers y la pongan a disposición de otros sitios.
Si tiene un plan Pro o superior, vaya a la pestaña de velocidad en el panel de control de Cloudflare y active “Priorización HTTP/2 optimizada” para acelerar su sitio.