[perf][infra] Холодная загрузка по мобильной сети: статика и API отдаются без сжатия (~3.7 МБ по проводу), у хэшированных ассетов нет cache-заголовков #346
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Суть
Холодная загрузка сервиса на мобильном интернете занимает многие секунды, и главная причина — не размер бандла (это #342), а способ доставки: сервер отдаёт всю статику и все API-ответы без сжатия и без кэш-заголовков. Замер по реальной прод-сборке:
На типичном мобильном канале 5–8 Мбит/с это ~4–6 секунд чистой передачи против ~1.5 c со сжатием — ещё до начала парсинга JS. Повторные заходы почти не выигрывают: ассеты content-hashed, но без явного
Cache-Controlбраузер живёт на эвристике, а мобильные браузеры агрессивно вытесняют кэш и перепроверяют ресурсы (даже 304 — это десятки запросов × 150–300 мс мобильного RTT).Диагноз
fastify-ip/multipart/cookie—@fastify/compressотсутствует в проекте.@fastify/staticв static.module.ts#L68-L71 зарегистрирован безpreCompressed, и вdist/нет.br/.gz-файлов. Дефолтный деплой (docker-compose.yml) отдаёт Node напрямую на:3000без реверс-прокси — сжимать некому вообще.Cache-Controlвыставлен только дляindex.html(no-cache— правильно, static.module.ts#L76). Все/assets/*content-hashed (index-yje0b65P.js) и могли бы кэшироваться навечно (immutable), но явных заголовков нет.index.html9 modulepreload-чанков + CSS + шрифты + локаль — на HTTP/1.1 упирается в лимит 6 соединений; на мобильном RTT очередь заметна. Node-приложение само по себе HTTP/2 не отдаёт.Границы изменения
apps/client/vite.config.ts(плагин пре-сжатия),apps/server/src/integrations/static/static.module.ts(preCompressed + кэш-заголовки),apps/server/src/main.ts(compress для API), опциональноdocker-compose.yml/README (рекомендация прокси). Поведение фич 1:1, меняется только доставка байтов. БД/API-контракты/MCP не затрагиваются.Решение
1. Пре-сжатие статики на этапе сборки (brotli + gzip)
Лучший вариант для self-hosted: сервер не тратит CPU на сжатие на лету, файлы сжимаются один раз при сборке.
2. Immutable-кэш строго для хэшированных ассетов
Важно:
immutableнельзя вешать на всю статику — вdist/кроме/assets/*лежат нехэшированные файлы (/locales/*.json,/vad/*, иконки, manifest), которые меняются между деплоями. Скоупить через отдельную регистрацию по префиксу либоsetHeaders:Результат: повторный заход = скачивание только
index.html(пара КБ) + API.3.
@fastify/compressдля динамических ответовapp.register(compress)в main.ts — сжимает API-JSON (контент страницы, дерево, комментарии) и переписанный share-SEO HTML (share-seo.controller.ts). Статику не трогает — она уже отдаётся pre-compressed. Проверить взаимодействие с SSE/stream-ответами AI-чата (исключить их из сжатия, см. крайние случаи).4. Документировать реверс-прокси с HTTP/2/3 (опционально)
Для инстансов за caddy/nginx/traefik — включённый HTTP/2 убирает лимит 6 соединений. Добавить в README/compose-пример опциональный caddy-фронт (авто-TLS + h2). Если прокси уже есть и на нём включён brotli — пп. 1/3 частично закрываются конфигом прокси, но
preCompressedвсё равно выгоднее (нулевой CPU на каждом запросе).Крайние случаи
index.htmlпереписывается при старте сервера (static.module.ts#L55-L64: инъекцияwindow.CONFIGчерез index-template) — пре-сжатую копию index.html создавать нельзя (будет отдаваться устаревший шаблон без конфига). Исключить из плагина сжатия (см. сниппет) и убедиться, что прямой запрос/index.htmlчерез fastifyStatic не находит.br-соседа.text/event-stream(у @fastify/compress есть настройка) либо убедиться, что дефолт не трогает эти маршруты.immutable(см. п. 2), иначе после деплоя пользователи застрянут на старых переводах/модели..br+.gzкопии увеличат слой с client/dist (~+30–40%, дешёво относительно выигрыша).Тесты / проверка
curl -H "Accept-Encoding: br" -sI https://<host>/assets/index-*.js→content-encoding: br+cache-control: …immutable.curl -sI …/index.html→ нетcontent-encodingот preCompressed (или корректный динамический),no-cache,window.CONFIGприсутствует в теле.curl -H "Accept-Encoding: br" -sI …/api/pages/...→ сжатый JSON.Вне скоупа
План работ
vite-plugin-compression2(br+gz, exclude index.html) +preCompressed: trueв static.module.ts./assets/*, no-cache для index.html, дефолт для остального (п. 2).@fastify/compressдля API/HTML с исключением SSE и бинарных mime (п. 3).curl -Iматрица + Lighthouse Slow 4G + повторная загрузка.