При работе с прогрессивными веб-приложениями (PWA) в экосистеме Next.js и Express разработчики часто сталкиваются с тем, что медиа-потоки (подкасты, аудиофайлы) ведут себя непредсказуемо на вкладках инкогнито или при первом («холодном») старте.

Экран плеера застывает в бесконечной буферизации, а в консоли рантайма происходят сетевые дедлоки. Давайте разберем внутреннее устройство стратегии кэширования CacheFirst и физику её взаимодействия с нативными аудио-потоками.

Часть 1. Разбор стратегии CacheFirst и физика аудио-запросов

Стратегия CacheFirst (Сначала Кэш) работает по принципу максимальной экономии сетевого трафика и обеспечения абсолютной оффлайн-доступности. Она идеально подходит для статичных медиа-файлов, которые не меняют своего содержимого со временем (например, скомпилированные .mp3 подкасты).

🛠️ Как устроен базовый алгоритм CacheFirst:

  1. Браузерный тег <audio> отправляет запрос на получение аудио-файла по его URL.
  2. Сервис-воркер перехватывает этот запрос и первым делом заглядывает в свое изолированное хранилище Cache Storage.
  3. Кэш-Хит (Данные найдены): Если файл уже был успешно сохранен ранее, воркер мгновенно возвращает его из оперативной памяти или диска устройства. Запрос в интернет даже не отправляется. Время ответа UI составляет < 1мс.
  4. Кэш-Мисс (Данных нет): Если файла в памяти нет (например, первый холодный старт в инкогнито), воркер отправляет запрос в сеть, скачивает файл, отдает его пользователю и одновременно сохраняет рабочую копию в кэш для всех последующих вызовов.
[ Браузер ] ──(1. Запрос)──> [ Service Worker ] 
                                   │
                     (2. Ищет в Cache Storage)
                                   │
         ┌─────────────────────────┴─────────────────────────┐
    [ Есть в Кэше ]                                    [ Нет в Кэше ]
         │                                                   │
  (3. Отдать из памяти)                               (4. Поход в Сеть)
         │                                                   │
         ▼                                            (5. Запись в Кэш)
 [ Звук играет < 1мс ]                                       │
                                                             ▼
                                                    [ Отдать пользователю ]

Почему пустой объект plugins: [{}] и статус 206 оживили инкогнито-режим?

Для воспроизведения тяжелых медиа-файлов браузерные движки (например, Blink/Chromium) никогда не скачивают файл целиком за один раз. Они используют механизм потокового аудио-стриминга.

Браузер отправляет к серверу запросы порциями, используя специальный заголовок Range: bytes=0- (дай мне байты с такого-то по такой-то). Сервер обрабатывает этот запрос и возвращает кусок аудио-данных с HTTP-статусом 206 Partial Content (Частичный контент).

В чём заключалась ловушка "Холодного старта" без плагина?

Когда сервис-воркер перехватывает Range-запрос со статусом 206, стандартные механизмы кэширования обычных стратегий (NetworkFirst или StaleWhileRevalidate) ломаются, так как они ожидают стандартный полноценный ответ 200 OK. Вкладка инкогнито зависает, потому что воркер пытается проглотить частичный поток как обычный статический файл.

Передача пустого конфигурационного объекта в массив плагинов принудительно активировала скрытую встроенную валидацию схем данных:

plugins: [
  // Активирует встроенный класс 'RangeRequests' на этапе генерации GenerateSW
  {} 
],
cacheableResponse: {
  // Жестко разрешаем воркеру перехватывать и укладывать в кэш частичные ответы
  statuses: [0, 200, 206],
}

Благодаря переводу .mp3 на рельсы CacheFirst в сочетании с явным разрешением статуса 206, встроенный компилятор развернул нативный обработчик RangeRequestsPlugin.

Теперь при самом первом клике в инкогнито воркер безошибочно распознает намерения браузера, корректно обрабатывает входящие Range-заголовки, и тег <audio> мгновенно начинает воспроизведение подкаста, не дожидаясь скачивания файла целиком.

Часть 2. Фундаментальные отличия от StaleWhileRevalidate и опасность сетевых дедлоков

Стратегия StaleWhileRevalidate (Устаревшее, пока проверяется) — это великолепный инструмент для текстовых страниц, бандлов скриптов, CSS-стилей или JSON-ответов API. Она позволяет пользователю мгновенно увидеть контент из памяти, пока воркер тихо обновляет его в фоне. Однако в мире медиа-стриминга эта стратегия становится смертельно опасной.

Как устроен алгоритм StaleWhileRevalidate на уровне сети:

  1. Браузер запрашивает ресурс.
  2. Сервис-воркер мгновенно возвращает старую (устаревшую) копию из кэша.
  3. В ту же секунду в фоне воркер открывает параллельное сетевое соединение, скачивает свежую версию файла целиком и перезаписывает кэш.
                  ┌──> [ Отдать старый кэш пользователю ] (Мгновенно)
                  │
[ Запрос к MP3 ] ─┤
                  │
                  └──> [ Фоновый скрытый запрос в сеть ] ──> [ Скачать файл целиком ] (Тяжелый трафик!)

Почему StaleWhileRevalidate ломает проигрывание подкастов?

Когда нативный тег <audio> отправляет запрос за первыми байтами звука (Range: bytes=0-), StaleWhileRevalidate перехватывает его и, следуя своей логике, пытается в фоне выкачать по сети весь огромный .mp3 файл целиком (весом 50–100 Мб), чтобы обновить кэш.

Из-за этого возникают две критические проблемы:

  • Сетевой затор (Network Congestion): Воркер забивает весь доступный сетевой канал вкладки одним тяжелым фоновым скачиванием.
  • Разрушение структуры Range: Тег <audio> ждет от воркера конкретный маленький кусочек данных (206 Partial Content) прямо сейчас, а воркер блокирует этот поток, пытаясь сначала сериализовать весь файл в памяти.

В результате в режиме Инкогнито плеер уходит в бесконечную буферизацию. Переключение на CacheFirst полностью решает эту проблему: воркер отдает данные порциями по требованию браузера и не совершает холостых фоновых скачиваний тяжелых медиафайлов.

Часть 3. Механика автоматической фоновой подкачки (Precache)

Ситуация, при которой сервис-воркер сразу после первой загрузки блога начинает агрессивно скачивать в терминале ресурсы, которые пользователь еще даже не открывал (например, logo-bash-matrix.gif), называется Прекэшированием (Precaching).

Как файлы прошлых статей оказываются в прекэше?

В плагине next-pwa для Next.js 11 по умолчанию включена автоматическая сборка манифеста Workbox.

  1. Во время компиляции проекта (next build) плагин сканирует вашу физическую папку public/static/.
  2. Он находит там абсолютно все файлы: картинки к статьям трехлетней давности, тяжелые шрифты, gif-анимации и локальные файлы заметок .mdx.
  3. Каждому файлу присваивается уникальный хэш-гуид (например, {url: "/static/img/...", revision: "3a3c871"}).
  4. Весь этот массив жестко «впекается» в тело вашего финального файла service-worker.js в команду e.precacheAndRoute([...]).

Когда новый пользователь (или вкладка Инкогнито) открывает блог, в воркере срабатывает событие install. Воркер считает своей главной обязанностью в ту же секунду скачать в фоне на устройство пользователя весь этот гигантский список файлов.

Как мы защитили сеть с помощью publicExcludes

Если прекэш забивает сеть, то при попытке включить подкаст браузер просто не может пробиться сквозь сотни параллельных запросов картинок. Чтобы разгрузить сеть и сделать кэширование ленивым (Lazy / Runtime Caching), мы применили снайперский фильтр:

publicExcludes: [
  '!static/img/**/*',       // Исключить картинки блогов из авто-скачивания
  '!static/_articles/**/*',  // Исключить mdx-файлы статей
  '!static/fonts/**/*'       // Исключить шрифты
]
  • Магия знака ! (отрицание): Мы явно приказали компилятору Workbox: «Вырежи эти папки из манифеста предварительной загрузки».
  • Результат: Теперь при холодном старте в Инкогнито сеть остается абсолютно пустой и чистой. Сервис-воркер больше не скачивает гифки и картинки втихаря. Он сохранит конкретную картинку или .mp3 подкаст в память браузера строго тогда, когда читатель сам кликнет по карточке или откроет статью.

Итоговая сравнительная таблица стратегий

КритерийCacheFirst (Наш выбор для аудио)StaleWhileRevalidate (Для статики)
Скорость ответа UIМгновенно (< 1мс)Мгновенно (< 1мс)
Поход в сетьТолько если файла физически нет в памятиПри каждом входящем запросе (в фоне)
Поведение контентаЗамораживает контент в памяти до истечения TTLТихо обновляет файлы к следующему визиту
Поддержка Range / Статус 206Да (Идеальная потоковая отдача порциями)Нет (Вызывает сетевой дедлок)
Расход трафика в ИнкогнитоМинимальный (строго по требованию плеера)Критический (пытается выкачать файл целиком)
Рекомендуется дляПодкастов, видео-отрывков, тяжелых шрифтовCSS-стилей, JS-бандлов, файлов локализации JSON

Заключение

Стабилизация PWA-ядра в Next.js 11 потребовала комплексного подхода:

  1. Перевод аудио-форматов на рельсы CacheFirst с поддержкой статуса 206 разблокировал нативный механизм Range-запросов браузера.
  2. Внедрение publicExcludes защитило вкладки от фонового забивания канала гифками и картинками.

Благодаря этому архитектурному союзу, подкасты на pravosleva.pro теперь запускаются в любой вкладке и на любом устройстве за доли миллисекунд с первого клика.

Другой ракурс

Схема 1. Потоковый аудио-стриминг через CacheFirst (Идеальный вариант)

В этом режиме Сервис-Воркер не блокирует сетевой поток, а работает как умный изоморфный прокси-сервер, нативно нарезая Range-запросы.

               [ ТЕГ <AUDIO> В БРАУЗЕРЕ ]
                           │
             1. Range Request (bytes=0-)
                           │
                           ▼
                 [ SERVICE WORKER ]
                           │
            2. Проверка в Cache Storage
                           │
       ┌───────────────────┴───────────────────┐
       ▼                                       ▼
 [ КЭШ-ХИТ: Есть ]                       [ КЭШ-МИСС: Пусто ]
       │                                       │
3. Чтение чанка (206)                   3. Поход в Сеть (HTTP)
       │                                       │
       ▼                                       ▼
[ Мгновенная отдача ]                   4. Стриминг байт из Сети
   (Время: < 1мс)                              │
       │                                5. Запись чанка (206)
       │                                       │
       ▼                                       ▼
 [ ЗВУК ИГРАЕТ ] <───────────────────────[ ОТДАТЬ В БРАУЗЕР ]

Схема 2. Сетевой дедлок подкаста через StaleWhileRevalidate (Проблемный вариант)

Здесь наглядно видно, как фоновое скрытое скачивание тяжелого файла целиком встает в жесткий клин с нативным тегом <audio> и блокирует Range-запросы.

               [ ТЕГ <AUDIO> В БРАУЗЕРЕ ]
                           │
             1. Range Request (bytes=0-)
                           │
                           ▼
                 [ SERVICE WORKER ]
                           │
       ┌───────────────────┴───────────────────┐
       ▼                                       ▼
[ ПОТОК А: Отдача кэша ]               [ ПОТОК Б: Апдейт Кэша ]
       │                                       │
2. Пытается выдать                      2. Открывает фоновый сокет
   старый кусок звука                      │
       │                                3. Начинает качать ВЕСЬ .mp3
       │                                   (Вес: 50-100 Мб целиком)
       │                                       │
       ▼                                       ▼
 ⚠️ СЕТЕВОЙ КЛИН ◄─────────────────────── ❌ ЗАБИЛ СЕТЕВОЙ КАНАЛ
 (Range-запросы заблокированы)             (Network Congestion)
       │
       ▼
 [ БЕСКОНЕЧНЫЙ ЛОАДЕР UI ]