HTTP/1.1, Head-of-Line Blocking и проблема в микрофронтендах.
Head-of-Line blocking
Head-of-Line (HOL) blocking — это проблема, которая возникает в сетевых системах, когда пакет данных или запрос блокирует обработку последующих пакетов, из-за чего другие пакеты вынуждены ожидать завершения обработки первого пакета. В контексте HTTP и сетевого трафика HOL blocking означает, что задержка одного запроса блокирует выполнение последующих запросов, снижая общую производительность системы.
При чем тут вообще микрофронтенды?
Мы можем использовать самые современные технологии на фронтенде, код может быть оптимизирован, все npm пакеты прошли проверку на вес в bundlephobia (вы же пользуетесь этим?), а наша страница может состоять из десятков микрофронтендов и грузиться они параллельно и всё это сделано, что бы клиент получил качественное приложение.
Но ваше веб-приложение может медленно загружаться из-за неправильно настроенной доставки статики и все ваши старания на оптимизацию приложения четны.
У админов есть такая фраза «работает — не трогай», и у вас на сайте может использоваться морально устаревший протокол HTTP/1.1.
Получается, это кликбейтное название, ведь затрагивает оно не только микрофронты? Получается так, а как вас, хипстеров, ещё заставить читать статью про сеть с ссылками на стандарты :).
Встретил я Head-of-Line Blocking вновь на микрофронтах, когда меня попросили посмотреть, почему у коллег виджет микрофронтовый долго появляется и чего-то ждёт, хотя module federation запрашивает все виджиты сразу, я увидел знакомую лесенку из десяток запросов и решил написать эту небольшую статью.
Но давайте всё же ещё раз разберёмся, почему для микрофронтендов это опасно. Представим, что у вас главная страница состоит из 12 виджетов, и только часть из них будут грузиться, остальная часть будет ждать, пока для них освободится очередь, и не факт, что самые важные для пользователя, будут грузиться первыми. Получается, что вашей главной странице часть виджетов будут грузиться со значительным отставанием.
Очень коротко
Я начну с триггеров проверки. Откройте devtools (или аналог в другом браузере), вкладка Perfomance, сделайте слепок профиля нагрузки при загрузки сайта, разверните колапсер с network, если у вас там лесенка, как на картинке 1, а на вкладке Network в поле Protocol* гордо красуется http/1.1 - идите к вашему админу/бэкендиру и просите, чтобы они сделали "что-то с этим", например, перевести все узлы на HTTP/2 и HTTP/3.

В том числе промежуточные балансеры, если у вас написано в браузере во вкладке network что это h2 (он же HTTP/2), не факт, что где-то по середине не стоит nginx с HTTP1.1 и тоже делает ограничение

Как читать картинку network в Perfomance.
На картинке 1 (Head-of-Line Blocking) мы видим сетевые запросы относительно времени. Обратите внимание, что в некоторых случаях запрос отображается сразу толстой цветной линией, а некоторые начинаются с тонкой линии, которая позже становится толстой. Тонкая линия - это когда был сделан запрос, непосредственно в коде или в документе, а его толстая часть - это реальный сетевой запрос в браузере. Ваш код делает "запрос" на сетевой запрос (простите за тавтологию) и будет все это время ожидать ответа, а сам сетевой запрос может стоять в очереди из-за, например при Head-of-Line Blocking.
Подробнее
В HTTP/1.1 браузеры обычно ограничивают количество одновременных соединений к одному домену до 6–8. Из-за этого ограничения, когда число запросов превышает этот лимит, часть запросов ставится в очередь. HOL blocking усугубляет эту ситуацию, так как даже при наличии свободных соединений другие запросы могут быть задержаны из-за медленного первого запроса в очереди, от этого мы и видим "лесенку".
Далее из стандарта пункт 6.3.2:
6.3.2. Pipelining A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MAY process a sequence of pipelined requests in parallel if they all have safe methods (Section 4.2.1 of [RFC7231]), but it MUST send the corresponding responses in the same order that the requests were received.
Когда вы открываете веб-страницу, браузер одновременно запрашивает несколько ресурсов, таких как CSS-файлы, изображения, скрипты и т. д. Если количество таких запросов превышает лимит (например, 10 запросов при лимите в 6 соединений), браузер сначала отправит 6 запросов. Остальные 4 будут поставлены в очередь и начнут загружаться только после завершения первых запросов.
Это приводит к следующей последовательности:
- Браузер устанавливает первые 6 соединений.
- Как только одно из них завершается, браузер запускает новый запрос из очереди.
Это ограничение действует на один домен. Если ваши запросы идут к разным доменам (например, к различным CDN), каждый из них может иметь свои лимиты. Подробнее тут.
Я ещё раз хочу обратить внимание, браузер ждёт завершение именного первого запроса, что бы начать следующий, при завершении первого, второй станет первым и уже его будут ждать.
Историческая справка по CDN, которая вам в целом и не нужна
CDN (Content Delivery Network) - это распределенная сеть серверов, главная цель которых, уменьшить задержку (latency), более подробно зачем использовать написано тут и тут. Но у CDN была ещё одна интересная особенность, незадокументированная фича, раньше браузеры кэшировали по адресу (uri) запрашиваемого ресурса, а не по адресу с которого идёт запрос. Например, какой-то сайт запросил jQuery https://code.jquery.com/jquery-1.12.4.min.js это закэшировалось в браузере, и теперь если ваш сайт запросит ту же версию jQuery с того же CDN (https://code.jquery.com/jquery-1.12.4.min.js) браузер отдаст из кэша без сетевого запроса. Это сильно повышало скорость загрузки сайта, мы отслеживали популярность версий jQuery, чтобы с большей вероятностью он был закэширован у пользователя.
Позже браузеры посчитали это за потенциальную уязвимость (и правильно) и начали и учитывали не только адрес ресурса, но и адресу с какого сайта был запрос.
История и эволюция лимитов соединений
Изначально спецификация HTTP/1.1 рекомендовала ограничивать количество одновременных соединений к одному серверу до 2, даже описали пример в стандарте RFC 2616 от 1999 года (ныне устаревшем) https://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4
Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.
В RFC 7230 (https://datatracker.ietf.org/doc/html/rfc7230), который, описывает актуальную спецификацию HTTP/1.1, нет явного ограничения на количество одновременных запросов. Эти ограничения и рекомендации обычно исходят из практических реализаций браузеров и серверов. Однако, в RFC 7230 есть некоторые упоминания о соединениях и их использовании, которые могут косвенно относиться к управлению параллельными запросами в разделе 6.3 и 6.3.2. Браузеры взяли в свои руки и сделали ограничения в 6-8 соединени .
Ограничения на уровне браузеров
- Google Chrome: Обычно разрешает до 6 соединений на домен.
- Mozilla Firefox: Тоже поддерживает до 6 соединений на домен.
- Microsoft Edge: Поддерживает 6-8 соединений на домен.
- Safari: Лимит также обычно составляет 6 соединений.
Обход ограничений
Что делать если я не могу уйти с HTTP/1.1 ?
Чтобы ускорить загрузку страниц, разработчики иногда прибегают к различным трюкам:
- Шардинг ресурсов по поддоменам: Разбивка ресурсов на несколько поддоменов (например, img1.example.com, img2.example.com) позволяет обходить ограничение в 6 соединений на домен. Например, если лимит — 6 соединений на один домен, то использование двух поддоменов позволяет увеличить количество одновременных запросов до 12.
- Параллельные загрузки через другие домены: Использование внешних CDN (например, для загрузки библиотек JavaScript или изображений) также помогает избежать лимита на соединения.
Решение проблемы в HTTP/2 и HTTP/3
В HTTP/2 была введена концепция мультиплексирования, которая позволяет отправлять несколько запросов и получать ответы по одному соединению одновременно. Это существенно уменьшает влияние head-of-line blocking и делает ненужным искусственное увеличение количества соединений.
В HTTP/3, который работает на базе протокола QUIC, эта проблема решена ещё более эффективно за счет отказа от блокировок на уровне транспортного протокола.
Итог
Максимальное количество одновременных соединений в HTTP/1.1 ограничено в среднем 6-8 соединениями на домен в современных браузерах. Это ограничение связано с особенностями работы протокола, и его можно частично обойти с помощью различных стратегий, но полностью проблема решается в более новых версиях протокола, таких как HTTP/2 и HTTP/3.
end
Подписывайся https://t.me/frontend_bookmark