Ситуация и симптомы
После развертывания Next.js 16 в Azure Container Apps за CDN‑слой был выставлен Azure Front Door Premium (Premium Tier) с Private Link. Приложение работало нормально, пока не появились странные задержки: HTML‑страницы отвечали быстро, запросы к API‑моделям тоже, но каждый JavaScript‑чанк, загружаемый через / _next/static/…, «висел» ровно 90 секунд. После этого браузер выдавал ERR_HTTP2_PROTOCOL_ERROR — HTTP/2‑поток завершался с INTERNAL_ERROR. Ошибка была одинаковой для всех чанков, а не только для отдельных файлов.
Архитектура и начальная конфигурация
- Next.js 16 развёрнут в Azure Container Apps внутри приватной сети (Private Link).
- Azure Front Door Premium функционирует как CDN и WAF‑защита.
- Трафик маршрутизируется по трём правилам:
/api/*— API‑эндпоинты;/_next/static/*— статические файлы и чанки;/*— «catch‑all» для SSR‑страниц.
- В
next.config.jsоставлена настройкаcompress: true(по умолчанию включена сервер‑сжатие).
Эти параметры выглядят типичными и не вызывают подозрений, однако в совокупности они создали конфликт.
Диагностика проблемы
1. Проверка простых запросов
Health‑probe от Front Door показывал 100 % здоровых ответов. Маленькие ответы (текстовые, JSON) отдавались мгновенно. SSR‑страница /sign-in (78 KB) загружалась за ~300 мс, что подтверждало корректную работу маршрута /*.
2. Сравнение запросов с и без gzip
Для того же файла app.js из каталога чанков были сделаны два curl‑запроса:
# без сжатия
curl -s -w "Total: %{time_total}s\n" -o /dev/null \
"https://my-fd-endpoint.azurefd.net/_next/static/chunks/app.js"
# → Total: 0.30s
# с запросом gzip
curl -s -w "Total: %{time_total}s\n" -o /dev/null \
-H "Accept-Encoding: gzip" \
"https://my-fd-endpoint.azurefd.net/_next/static/chunks/app.js"
# → Total: 90.24s
Тот же файл, тот же путь, та же инфраструктура — различие только в заголовке Accept-Encoding. При запросе gzip ответ приходил почти ровно через 90 секунд и завершался ошибкой HTTP/2.
3. Анализ заголовков
Ответ на «сломанный» запрос выглядел так:
HTTP/2 200
content-type: application/javascript; charset=UTF-8
content-length: 112049
cache-control: public, max-age=31536000, immutable
content-encoding: gzip
vary: Accept-Encoding
x-cache: TCP_MISS
Значение content-length указывало размер незжатого файла (112 KB), хотя в заголовке уже присутствовал content-encoding: gzip. Таким образом Front Door ожидал получить ровно 112 KB «сырых» байтов, но вместо этого получил уже сжатый поток, размер которого был меньше. При попытке отправить недостающие байты соединение закрывалось, и HTTP/2‑слой генерировал INTERNAL_ERROR. В результате браузер отключал запрос и выводил ошибку.
Почему возникла задержка
Корень проблемы — двойное сжатие.
- На уровне origin (контейнер с Next.js) включена серверная компрессия (
compress: true). При запросеAccept-Encoding: gzipприложение уже отдаёт gzip‑сжатый файл. - Azure Front Door по умолчанию также пытается сжать ответ, если клиент просит gzip. При этом он оставляет оригинальный
Content‑Lengthбез пересчёта, полагая, что сжатие будет выполнено на уровне CDN.
В результате клиент получает заголовок, указывающий на размер несжатого тела, но фактически получает уже сжатый поток. HTTP/2‑механизм, ожидающий ровно указанный размер, ждёт недостающие байты до истечения таймаута (≈ 90 сек). После таймаута поток завершается с INTERNAL_ERROR.
Решение и настройка
Отключить компрессию на одном из уровней
Самый простой способ — выключить сжатие либо в приложении, либо в Front Door, оставив его только на одной точке. В данном случае было решено оставить компрессию в CDN, так как Front Door умеет кэшировать и обслуживать сжатый контент более эффективно.
Шаги:
-
В
next.config.jsубрать серверную компрессию:// next.config.js module.exports = { compress: false, // отключаем gzip на уровне Next.js }; -
Убедиться, что в правилах Front Door включена опция «Compression». По умолчанию Premium‑tier поддерживает автоматическое сжатие для MIME‑типов
text/*,application/javascript,application/jsonи т.д. -
Проверить кэш‑заголовки:
cache-control: public, max-age=31536000, immutableостаются неизменными, что позволяет CDN хранить сжатый файл длительное время без повторных запросов к origin.
После перезапуска контейнеров и обновления конфигурации Front Door, запросы к / _next/static/* начали отвечать за < 200 мс как без gzip, так и с gzip, без ошибок HTTP/2. Плюс к этому сократилось потребление CPU на стороне origin, поскольку теперь сжатие выполняет только CDN.
Рекомендации по использованию компрессии
- Определяйте единственный источник сжатия: либо приложение, либо CDN. Дублирование приводит к несоответствиям в
Content‑Lengthи потенциальным таймаутам. - Проверяйте заголовки при включении gzip:
Content-EncodingиContent-Lengthдолжны быть согласованы. Если они расходятся, проверьте, не происходит ли двойное сжатие. - Тестируйте с
Accept-Encodingкак часть базовой диагностики. Инструментыcurlи браузерные DevTools позволяют быстро увидеть разницу в времени отклика. - Следите за конфигурацией Health Probes Front Door: они не проверяют сжатый контент, поэтому могут показывать «здоровье», пока пользователи сталкиваются с таймаутами.
- Используйте кэш‑контроль (
immutable, длительныеmax-age) для статических файлов Next.js, чтобы CDN мог обслуживать их без обращения к origin.
Применив эти принципы, можно избежать ситуаций, когда простая настройка компрессии превращает обычный веб‑приложение в «медленный монстр» с 90‑секундными задержками. Azure Front Door остаётся мощным инструментом для ускорения доставки, но требует тщательной синхронизации параметров с бекендом.