Почему вашему сайту стоит отправлять меньше запросов, а не просто уменьшать их размер
Крошечные файлы не бесплатны. Современный HTTP уменьшил накладные расходы запросов, но не сделал их неважными.
Содержание
- Удобный миф о том, что нужно «просто сделать каждый файл меньше»
- Запросы — это не только байты
- «Но HTTP/2 это исправил» — в основном нет
- Истина живёт в waterfall
- Маленькие файлы всё ещё важны — просто не одинаково
- Скрытые издержки множества маленьких файлов
- 1. Позднее обнаружение
- 2. Накладные расходы headers
- 3. Прерывания main thread
- 4. Сложность cache
- Bundling вернулся, но с рассудительностью
- Сторонние запросы заслуживают особого подозрения
- Практический чеклист по сокращению запросов
- Удалить
- Объединять осмысленно
- Откладывать
- Правильно кэшировать
- Измерять заново
- Как выглядит хороший результат
Удобный миф о том, что нужно «просто сделать каждый файл меньше»
Годами советы по веб-производительности звучали просто: сжимайте всё, минифицируйте всё, уменьшайте каждый asset.
В целом этот совет по-прежнему верен. Скрипт на 40 KB обычно лучше скрипта на 400 KB. Оптимизированное изображение лучше сырого экспорта. Brotli, AVIF, минификация CSS, tree shaking и font subsetting — всё это важно.
Но на многих production-сайтах главная проблема уже не в одном слишком большом файле. Она в количестве вещей, которые браузеру нужно запросить, прежде чем страница начнёт ощущаться пригодной к использованию.
Страница может выглядеть дисциплинированной по размерам файлов и всё равно быть медленной, потому что отправляет 120 запросов: фрагменты CSS, JavaScript-чанки, сторонние теги, файлы шрифтов, tracking pixels, icon sprites, JSON endpoints, preloads, analytics beacons и cache revalidations. Каждый из них может быть «маленьким». Вместе они создают длинный и хрупкий waterfall.
Практическое правило такое: когда отдельные assets уже разумно сжаты, сокращение количества запросов часто улучшает пользовательский опыт сильнее, чем экономия нескольких килобайт в каждом файле.
Запросы — это не только байты
Сетевой запрос — это не просто передача данных. Это последовательность работы.
Браузер должен обнаружить ресурс, решить, когда его загружать, поставить его в очередь относительно других ресурсов, отправить headers, дождаться сервера, получить headers, разобрать ответ, часто распаковать его, а затем сделать с ним что-то полезное.
Это «что-то полезное» может быть дорогим. JavaScript-файл нужно распарсить, скомпилировать и выполнить. CSS-файл может блокировать rendering. Файл шрифта может задерживать читаемый текст или вызывать layout shifts. Изображение может влиять на элемент Largest Contentful Paint. Сторонний скрипт может принести собственную цепочку зависимостей.
Именно поэтому количество запросов всё ещё важно, даже когда файлы маленькие. Скрипт на 3 KB может быть хуже изображения на 30 KB, если он блокирует rendering, приходит поздно и выполняется в main thread в неподходящий момент.
Если вы читаете отчёт Lighthouse и чувствуете, что вас наказывают десятками отдельных предупреждений, начните с request waterfall, а не с отдельных оценок. У нас есть практическое руководство о том, как читать отчёт Lighthouse без паники, но краткая версия такая: найдите, что блокирует first render и что задерживает основной контент.
«Но HTTP/2 это исправил» — в основном нет
HTTP/2 и HTTP/3 изменили экономику запросов. Они принесли multiplexing, сжатие headers и более эффективное поведение соединений. Проще говоря, браузеры стали гораздо лучше отправлять множество запросов через меньшее число соединений.
Это было настоящим улучшением. Оно также убило некоторые старые привычки, например экстремальные CSS sprites и гигантские склеенные bundles, созданные только ради обхода лимитов соединений.
Но HTTP/2 не сделал запросы бесплатными.
Multiplexing помогает, когда многие ресурсы используют одно соединение, но браузеру всё равно нужно расставлять им приоритеты. Серверам всё равно нужно отвечать. Клиенту всё равно нужно обрабатывать каждый ответ. Перегрузка сети, packet loss, TLS negotiation, DNS lookup, cache misses и нагрузка на main thread никуда не делись.
HTTP/3 улучшает некоторые аспекты транспорта, особенно connection migration и head-of-line blocking на транспортном уровне. Он не убирает стоимость обнаружения, планирования, загрузки, парсинга и выполнения ресурсов.
Поэтому современная цель — не «собрать всё в один огромный файл». Цель — «отправлять меньше критических запросов и делать оставшиеся запросы осознанными».
Истина живёт в waterfall
Проблемы производительности редко заявляют о себе одной метрикой. Они проявляются как форма.
Откройте network panel в браузере и посмотрите на первые несколько секунд. Спросите:
- Сколько запросов стартует до появления основного контента?
- Какие запросы блокируют rendering?
- Обнаруживаются ли важные ресурсы поздно?
- Конкурируют ли сторонние скрипты с first-party CSS, шрифтами или изображениями?
- Много ли файлов возвращают ответы 304 вместо того, чтобы отдаваться напрямую из cache?
- Разделены ли icons, fonts или UI fragments на большее количество файлов, чем нужно странице?
У быстрой страницы ранний waterfall обычно скучный. Небольшое количество критических ресурсов приходит рано. Некритические ресурсы ждут. Сторонние скрипты отложены, ограничены или удалены. Браузер не вынужден жонглировать двадцатью приоритетами, прежде чем сможет нарисовать страницу.
У медленной страницы waterfall часто нервный: много маленьких файлов, много origins и много поздних обнаружений.
Маленькие файлы всё ещё важны — просто не одинаково
Это не аргумент против сжатия или оптимизации. Это аргумент против оптимизации байтов при игнорировании координации.
Меньший размер файлов важнее всего, когда ресурс большой, блокирует rendering или находится на пути к основному контенту. Например:
- Hero image должен быть правильно подобран по размеру и закодирован.
- Render-blocking CSS должен быть лёгким.
- JavaScript, нужный для первого взаимодействия, должен быть минимальным.
- Шрифты должны быть subset, сжаты и ограничены теми weights, которые действительно используются.
Шрифты — частый пример. Команды часто зацикливаются на том, весит ли файл шрифта 24 KB или 31 KB, при этом отправляя шесть weights, два styles и несколько families. Лучшее исправление — не сбрить 7 KB с одного файла. Лучшее исправление — отправлять меньше файлов шрифтов. Если типографика входит в вашу работу над производительностью, web fonts всё ещё остаются одним из самых простых выигрышей на большинстве сайтов.
С изображениями работает та же схема. AVIF или WebP могут заметно экономить байты, но отправлять десять декоративных изображений above the fold — всё равно плохой план. Выбирайте лучшие форматы, да, но также задавайтесь вопросом, нужно ли вообще запрашивать каждое изображение. Для решений по форматам наше руководство когда AVIF лучше WebP, а когда нет хорошо дополняет работу над количеством запросов.
Скрытые издержки множества маленьких файлов
Множество маленьких запросов часто создаёт проблемы, которые не видны, если смотреть только на общий объём переданных байтов.
1. Позднее обнаружение
Браузеры не могут запросить то, чего они ещё не обнаружили. CSS-файл может ссылаться на шрифт. Скрипт может import другой скрипт. Компонент может запросить JSON после hydration. Каждая зависимость создаёт ещё один шаг в цепочке.
Чем глубже цепочка, тем позже начинается важная работа.
2. Накладные расходы headers
Каждый запрос и ответ включает headers. Сжатие headers помогает, особенно поверх HTTP/2 и HTTP/3, но не устраняет накладные расходы. Cookies могут сильно усугубить ситуацию. Если ваш сайт отправляет большие cookies с каждым запросом, крошечные assets на практике перестают быть такими уж крошечными.
Это одна из причин, почему static assets часто должны жить на путях или доменах без cookies, и почему cache headers заслуживают внимания. Если headers ведут себя странно в production, отладка redirects и HTTP headers обычно быстрее, чем догадки.
3. Прерывания main thread
Множество JavaScript-чанков может создавать повторяющуюся работу по парсингу и выполнению. Даже если каждый чанк мал, браузер может постоянно останавливаться, чтобы оценить код. Это может вредить Interaction to Next Paint и делать страницу дёрганой.
Пользователю не важно, что каждый файл был маленьким. Ему важно, что нажатие на меню заняло 600 миллисекунд.
4. Сложность cache
Разделение assets может улучшить caching, если выполнено аккуратно. Стабильный vendor bundle и изменяющийся app bundle могут быть хорошим разделением.
Но чрезмерный chunking может дать обратный эффект. Больше файлов означает больше cache lookups, больше возможностей revalidation, больше координации версий и больше способов случайно инвалидировать ресурсы, которым не нужно было меняться.
Bundling вернулся, но с рассудительностью
Первая эпоха веб-производительности любила bundling, потому что у браузеров были строгие лимиты соединений. Потом пришёл HTTP/2, и многие команды резко качнулись в сторону агрессивного code splitting. Часть этого была полезной. Часть стала суеверием.
Разумная середина — route-aware bundling.
Для типичного marketing site или content site:
- Inline или загружайте только CSS, нужный для initial rendering.
- Держите global JavaScript маленьким.
- Не разделяйте крошечные modules на отдельные network requests.
- Откладывайте интерактивные функции, которые не нужны сразу.
- Удаляйте сторонние скрипты, которые не оправдывают свою стоимость.
Для приложения:
- Делите по route или крупной feature, а не по каждому component.
- Держите shared dependencies стабильными и cacheable.
- Preload только те ресурсы, которые точно скоро понадобятся.
- Не загружайте код admin, dashboard, editor или experiments на публичных страницах.
- Измеряйте стоимость взаимодействия, а не только размер bundle.
Bundling не является автоматически хорошим. Code splitting не является автоматически хорошим. Полезный вопрос звучит так: помогает ли это разделение браузеру быстрее доставить следующий значимый пользовательский опыт?
Сторонние запросы заслуживают особого подозрения
First-party requests хотя бы находятся под вашим контролем. Third-party requests часто медленнее, менее предсказуемы и дороже, чем выглядят.
Один tag manager может запускать analytics, ads, heatmaps, chat widgets, A/B testing, consent tools и personalization scripts. Каждый vendor может принести дополнительные запросы. Некоторые запустятся рано. Некоторые заблокируют main thread. Некоторые изменятся без вашего release process.
Лучшая оптимизация стороннего кода — удаление. Вторая по эффективности — задержка.
Перед добавлением стороннего скрипта спросите:
- Должен ли он загружаться до того, как пользователь увидит страницу?
- Должен ли он загружаться на каждой странице?
- Может ли он загружаться после consent, взаимодействия или idle time?
- Кто внутри компании за него отвечает?
- Какая метрика доказывает, что он стоит своей performance cost?
Здесь производительность становится governance. У кого-то должно быть право сказать нет.
Практический чеклист по сокращению запросов
Начните со страниц, которые важнее всего: homepage, pricing page, product page, checkout, signup или top landing pages. Затем пройдитесь по waterfall.
Удалить
- Удалите неиспользуемые JavaScript и CSS.
- Уберите старые experiments, заброшенные pixels и дублирующую analytics.
- Откажитесь от неиспользуемых font weights и icon libraries.
- Замените декоративные изображения на CSS там, где это уместно.
Объединять осмысленно
- Собирайте в bundle крошечные JavaScript modules, которые всегда загружаются вместе.
- Объединяйте маленькие CSS-файлы, блокирующие один и тот же render path.
- Используйте SVG sprites или inline SVG для повторяющихся icons, если это сокращает запросы без вреда для поддерживаемости.
Откладывать
- Lazy-load изображения below the fold.
- Откладывайте некритические скрипты до after first paint или пользовательского взаимодействия.
- Загружайте comments, embeds, maps, chat и video players только тогда, когда они нужны.
Правильно кэшировать
- Используйте long-lived caching для versioned static assets.
- Избегайте лишней revalidation для файлов, которые редко меняются.
- Держите HTML свежим, но позволяйте hashed assets оставаться в cache.
Измерять заново
После каждого изменения снова проверяйте waterfall. Цель — не идеальный score. Цель — меньше критических запросов, более ранний полезный rendering и меньше нарушений работы main thread.
Как выглядит хороший результат
У здоровой страницы не обязательно должно быть минимально возможное количество запросов. У неё должен быть небольшой и продуманный critical path.
Браузер получает HTML, essential CSS, основное изображение контента, если оно есть, возможно небольшой скрипт, нужный для navigation или взаимодействия above the fold, и минимальный набор шрифтов, необходимый, чтобы текст был читаемым. Всё остальное ждёт своей очереди.
В этом разница между страницей, которая просто оптимизирована, и страницей, которая ощущается быстрой.
Уменьшать файлы всё ещё стоит. Но если сайт уже разумно сжат, следующий выигрыш в производительности обычно не в очередных 2 KB, сэкономленных в bundle. Он в одном лишнем blocking request, которого больше нет, в одном лишнем файле шрифта, в одном лишнем стороннем скрипте, в одной лишней цепочке зависимостей.
Меньше запросов — проще работа браузера. А простота оказывается быстрой чаще, чем нам хочется признавать.