Service Worker и стратегии кэширования в 2026: Workbox, stale-while-revalidate и offline-first

Полный разбор Service Worker и стратегий кэширования в 2026: Workbox 7, stale-while-revalidate для статики, NetworkFirst для HTML, Navigation Preload и офлайн-фолбэки с рабочими примерами кода.

Service Worker: кэширование 2026 Workbox

Обновлено: 31 мая 2026

Service Worker и стратегии кэширования в 2026 — это слой между сетью и страницей, который перехватывает HTTP-запросы и решает, отдать ответ из CacheStorage, сходить в сеть или показать офлайн-фолбэк. Правильно настроенная стратегия (stale-while-revalidate для статики, network-first для HTML, cache-only для иммутабельных ассетов) сокращает повторный TTFB до 5–20 мс, делает приложение работоспособным офлайн и снижает нагрузку на CDN на 40–70%. Дальше разбираем актуальные паттерны на Workbox 7, чистом Service Worker API и интеграцию с Cache-Control в 2026 году.

  • Service Worker в 2026 поддерживается во всех современных браузерах (Chrome 50+, Firefox 44+, Safari 11.1+, Edge 17+) и работает только по HTTPS или на localhost.
  • Cache API независим от HTTP-кэша браузера: у него собственная квота (обычно ≥60% свободного диска) и отдельные правила инвалидации.
  • Workbox 7.3 остаётся стандартом де-факто: 14 КБ gzip ядро, готовые рецепты для stale-while-revalidate, NetworkFirst, ExpirationPlugin и Background Sync.
  • Стратегия выбирается по типу ресурса: HTML идёт через NetworkFirst с таймаутом 3 с, JS/CSS с хэшем через CacheFirst, API через StaleWhileRevalidate с короткой максимальной возрастной отметкой.
  • Navigation Preload параллельно стартует сетевой запрос, пока Service Worker просыпается, и убирает 100–300 мс задержки на холодный старт.
  • Service Worker не ускоряет первый визит, его выгода проявляется со второго захода; считайте экономию в RUM через navigation.deliveredFromCache.

Что такое Service Worker и зачем он нужен в 2026

Если коротко: Service Worker, это JavaScript-воркер, который браузер запускает в отдельном потоке, без доступа к DOM, и держит активным даже после закрытия вкладки (с ограничениями ОС). Он регистрируется один раз через navigator.serviceWorker.register() и работает как программируемый прокси. На каждый fetch() страницы срабатывает событие fetch внутри воркера, и вы решаете, что вернуть в respondWith().

В 2026 году Service Worker нужен для трёх типичных сценариев. Первый, офлайн-режим: PWA, CRM-инструменты, читалки, всё, что должно открыться без сети. Второй, мгновенная повторная загрузка: предзагруженные шрифты и иконки моментально появляются на втором визите. Третий, снижение стоимости CDN, потому что кэш живёт у клиента, и за повторный запрос вы не платите за исходящий трафик. Согласно HTTP Archive 2026, среди топ-10 000 сайтов 38% используют Service Worker, и у них медианный LCP при повторном визите на 410 мс ниже.

Ограничения 2026 года тоже стоит держать в голове. Воркер работает только по HTTPS (исключение, localhost для разработки), scope ограничен директорией регистрации, нельзя получить доступ к localStorage или DOM, а в Safari iOS возможен принудительный выгруз через 30 дней неактивности с очисткой Cache Storage. Полное API описано в документации MDN по Service Worker API.

Cache API против HTTP-кэша: чем отличаются

Главное недоразумение, которое я постоянно вижу в код-ревью: разработчики думают, что Cache API и HTTP-кэш браузера, это одно и то же. На самом деле это два разных хранилища с разной логикой инвалидации, разными квотами и разным API доступа. Понимание границы между ними, по сути, фундамент корректной перф-стратегии.

СвойствоHTTP-кэш браузераCache API (Service Worker)
ИнвалидацияПо заголовкам Cache-Control, ETag, Last-ModifiedПолностью ручная, через cache.delete() или версионирование
КвотаДелится на весь браузер, агрессивно выселяетсяДо 60% свободного диска на origin, с navigator.storage.persist() защищено от выселения
Программный доступНет (управляется только заголовками)Полный: caches.open(), match(), put(), keys()
Работа офлайнТолько если ресурс в кэше и срок не истёкПолностью контролируется кодом, включая фолбэк-страницы
Учитывает VaryДа, автоматическиДа, но требуется ignoreVary: true для исключений
Действует наТекущая вкладка после перезагрузкиВсе вкладки в scope, включая офлайн-старт

Практический вывод. Service Worker НЕ заменяет HTTP-кэш, он работает поверх него. Если у ответа есть Cache-Control: max-age=31536000, immutable, браузер вообще не пойдёт в Service Worker для повторного запроса. Он просто отдаст из памяти. Поэтому стратегия 2026 такая: используйте долгие HTTP-заголовки для иммутабельных ассетов (JS/CSS с хэшем в имени), а Service Worker оставьте для HTML, API и офлайн-фолбэков.

Пять стратегий кэширования и когда их применять

Workbox в своё время формализовал пять базовых стратегий, и в 2026 они остаются каноном. Выбор стратегии, по сути, всегда компромисс между свежестью и скоростью. Ниже таблица соответствия типа ресурса и оптимальной стратегии, основанная на опыте десятков продакшен-аудитов.

1. Cache First (cache-first, fallback на сеть)

Сначала ищем в кэше, и только если ничего не найдено, идём в сеть и кладём результат в кэш. Самый быстрый вариант, ноль сетевых запросов на повторных загрузках. Применять только к ресурсам, которые НЕ МЕНЯЮТСЯ: иконки, шрифты, JS/CSS с хэшем в имени, изображения с уникальным URL. Если случайно поставить эту стратегию на HTML, пользователь застрянет на старой версии до ручной чистки кэша. Я однажды наблюдал, как команда два дня дебажила «зависший» релиз, ровно по этой причине.

2. Network First (network-first, fallback на кэш)

Сначала пытаемся загрузить из сети с таймаутом 3 секунды. Если сеть отвалилась или медленная, отдаём из кэша. Идеально для HTML-навигации, новостных лент, API с быстро меняющимися данными. Минус один: на быстрой сети добавляется один round-trip к каждому запросу, поэтому стратегия конкурирует с предзагрузкой.

3. Stale While Revalidate (SWR)

Возвращаем из кэша СРАЗУ, но параллельно запускаем сетевой запрос и обновляем кэш, чтобы следующий визит увидел свежее. В 2026 это стратегия по умолчанию для большинства типов: статики, шрифтов, аватаров, разметки списков. Пользователь всегда получает мгновенный ответ, а кэш сам себя поддерживает. Точно так же работает Cache-Control: stale-while-revalidate=60 на уровне HTTP, и оба механизма можно комбинировать.

4. Network Only

Никогда не лезть в кэш. Для POST/PUT/DELETE, форм входа, платежей, аналитики. Service Worker всё равно полезен, потому что можно сделать Background Sync для повторной отправки, если сеть упала прямо в момент клика.

5. Cache Only

Никогда не идти в сеть. Применяется в связке с предкэшированием (precache): на этапе install кладём в кэш минимальный shell приложения (HTML, CSS, главные JS), и потом отдаём только из него. Используется для офлайн-first приложений и для офлайн-фолбэка.

Workbox 7: быстрый старт за 30 минут

Workbox 7.3 (Google, MIT, 14 КБ gzip ядро) остаётся стандартом де-факто для PWA в 2026. Он даёт декларативный API, готовые плагины для инвалидации, Background Sync, аналитики офлайн-визитов и интегрируется с Vite, Webpack, Next.js и Astro. Вот минимальная конфигурация Service Worker, которая закрывает 80% случаев.

// sw.js: собирается через workbox-build или vite-plugin-pwa
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// 1. Прекэш файлов из манифеста сборки (injected by build tool)
precacheAndRoute(self.__WB_MANIFEST);

// 2. HTML-навигация → NetworkFirst с таймаутом 3 секунды
registerRoute(
  new NavigationRoute(
    new NetworkFirst({
      cacheName: 'html-cache',
      networkTimeoutSeconds: 3,
      plugins: [new CacheableResponsePlugin({ statuses: [200] })],
    })
  )
);

// 3. JS/CSS с хэшем → CacheFirst (они иммутабельны)
registerRoute(
  ({ request }) => request.destination === 'script' || request.destination === 'style',
  new CacheFirst({
    cacheName: 'static-assets',
    plugins: [
      new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
    ],
  })
);

// 4. Изображения → StaleWhileRevalidate с лимитом 100 элементов
registerRoute(
  ({ request }) => request.destination === 'image',
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 7 * 24 * 60 * 60, purgeOnQuotaError: true }),
    ],
  })
);

// 5. API GET-запросы → StaleWhileRevalidate
registerRoute(
  ({ url, request }) => url.pathname.startsWith('/api/') && request.method === 'GET',
  new StaleWhileRevalidate({ cacheName: 'api-cache' })
);

// 6. Skip waiting: обновляться сразу после установки
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));

Регистрация на клиенте, одна строка, и желательно после load, чтобы не конкурировать с критическим рендерингом:

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js', { scope: '/' })
      .then(reg => console.log('SW зарегистрирован, scope:', reg.scope))
      .catch(err => console.error('SW регистрация упала:', err));
  });
}

Для Vite/React/Vue берите vite-plugin-pwa, он автогенерирует __WB_MANIFEST с хэшами файлов и предлагает шаблоны конфигов. Для Next.js, @ducanh2912/next-pwa (форк next-pwa, активно поддерживается в 2026).

Чистый Service Worker без библиотек

Честно говоря, иногда Workbox это оверкилл: небольшой сайт, лендинг, документация. Шестьдесят строк чистого JS дают офлайн-фолбэк и кэширование статики. Главное, версионируйте имя кэша, чтобы новая версия Service Worker удалила старые записи. Это особенно важно при частых релизах, где имена ассетов меняются вместе с хэшами.

// sw.js: 60 строк, без зависимостей
const VERSION = 'v3-2026-05-31';
const STATIC_CACHE = `static-${VERSION}`;
const PAGES_CACHE = `pages-${VERSION}`;
const PRECACHE_URLS = ['/', '/offline.html', '/styles/main.css', '/scripts/app.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => !k.endsWith(VERSION)).map((k) => caches.delete(k)))
    ).then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', (event) => {
  const { request } = event;
  if (request.method !== 'GET') return;
  // HTML: NetworkFirst с офлайн-фолбэком
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request)
        .then((res) => {
          const copy = res.clone();
          caches.open(PAGES_CACHE).then((c) => c.put(request, copy));
          return res;
        })
        .catch(() => caches.match(request).then((r) => r || caches.match('/offline.html')))
    );
    return;
  }
  // Статика: StaleWhileRevalidate
  event.respondWith(
    caches.match(request).then((cached) => {
      const network = fetch(request).then((res) => {
        const copy = res.clone();
        caches.open(STATIC_CACHE).then((c) => c.put(request, copy));
        return res;
      }).catch(() => cached);
      return cached || network;
    })
  );
});

Чистый код выигрывает, когда вам нужен полный контроль и предсказуемый размер бандла SW. Если же нужны Background Sync, аналитика офлайн-просмотров или сложная очистка по LRU, переходите на Workbox без раздумий.

Самый известный антипаттерн Service Worker такой: при первом переходе на страницу браузеру нужно «разбудить» SW, что добавляет 100–300 мс к TTFB на мобильных устройствах. NavigationPreload решает эту проблему. Браузер параллельно стартует сетевой запрос за HTML, пока SW просыпается, и передаёт готовый ответ в event.preloadResponse. По сути, это бесплатный выигрыш для любой network-first или stale-while-revalidate стратегии.

self.addEventListener('activate', (event) => {
  event.waitUntil(
    (async () => {
      if (self.registration.navigationPreload) {
        await self.registration.navigationPreload.enable();
      }
      await self.clients.claim();
    })()
  );
});

self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith((async () => {
    try {
      const preload = await event.preloadResponse;
      if (preload) return preload;
      return await fetch(event.request);
    } catch {
      return (await caches.match(event.request)) ||
             (await caches.match('/offline.html'));
    }
  })());
});

Workbox включает Navigation Preload автоматически в NavigationRoute начиная с 6.x. Эффект реально измеряем: на нашем тестовом стенде LCP холодной навигации просел с 1420 мс до 980 мс на Moto G Power. Детали есть в руководстве web.dev по Navigation Preload.

Как обновить Service Worker без потери данных

Жизненный цикл SW, по моему опыту, главный источник багов в продакшене. Когда вы деплоите новую версию sw.js, браузер видит изменения только при следующей навигации, скачивает новый файл, кладёт его в состояние waiting и НЕ активирует, пока не закроются все вкладки со старой версией. Это безопасно (нет конфликтов между версиями), но раздражает пользователей: они видят старую версию, пока не закроют ВСЕ вкладки сайта. Я однажды поймал этот баг при выкатке хотфикса, потерял часа три, пока сообразил, в чём дело.

Три рабочих паттерна обновления в 2026:

  1. Skip waiting автоматически (self.skipWaiting() в install). Подходит, если новая версия совместима со старой. Опасно для API-контрактов, которые поменялись.
  2. Skip waiting по запросу пользователя. Показываете тост «Доступна новая версия, обновить?», по клику отправляете в SW сообщение postMessage({type: 'SKIP_WAITING'}), и в SW делаете self.skipWaiting(). Самый безопасный вариант, на мой взгляд.
  3. Полная перезагрузка. Слушаете controllerchange и делаете location.reload(). Гарантирует, что страница и SW синхронизированы.
// На стороне страницы
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});

navigator.serviceWorker.register('/sw.js').then((reg) => {
  reg.addEventListener('updatefound', () => {
    const newSW = reg.installing;
    newSW.addEventListener('statechange', () => {
      if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
        showUpdateToast(() => newSW.postMessage({ type: 'SKIP_WAITING' }));
      }
    });
  });
});

Отладка, метрики и RUM

Service Worker, это код, который не виден на странице, поэтому без приборов вы летите вслепую. Три уровня диагностики помогут держать SW под контролем как в разработке, так и в проде.

Локально: Chrome DevTools → Application → Service Workers. Чек-боксы «Update on reload», «Bypass for network», «Offline» переключают режимы за секунду. Вкладка Cache Storage показывает все кэши и их содержимое, можно вручную удалить запись и проверить ребилд. В Firefox используйте about:debugging#/runtime/this-firefox.

В продакшене (RUM): Performance API даёт два ключевых поля. Поле performance.getEntriesByType('navigation')[0].deliveredFromCache возвращает true, если ответ пришёл из CacheStorage. А performance.getEntriesByType('resource') для каждого ресурса содержит workerStart и workerDuration, сколько SW потратил на обработку. Если workerDuration > 50 мс, ваш fetch-листенер делает слишком много синхронной работы. Проверьте, нет ли там тяжёлой логики.

// Отправка метрик SW в аналитику
const nav = performance.getEntriesByType('navigation')[0];
if (nav) {
  analytics.send('sw_metrics', {
    delivered_from_cache: nav.deliveredFromCache,
    worker_start: nav.workerStart,
    response_time: nav.responseEnd - nav.requestStart,
  });
}

Связка с другими перф-инструментами разобрана в нашем материале про приоритизацию загрузки ресурсов в 2026. Service Worker логически живёт после preconnect и preload, но до парсинга HTML, и взаимодействует с обоими.

Частые ошибки и как их избежать

За четыре года ревью PWA-проектов я видел одни и те же грабли. Краткий чек-лист, который сэкономит вам неделю отладки:

  • Кэширование sw.js: никогда не позволяйте CDN кэшировать сам файл Service Worker дольше нескольких минут. Иначе пользователи никогда не получат новую версию. Ставьте Cache-Control: no-cache или max-age=60 на sw.js.
  • Кэширование POST-запросов: Cache API физически не умеет хранить POST. Не пытайтесь, используйте Background Sync.
  • Один кэш на всё: смешивая HTML, JS и изображения в одном кэше, вы теряете возможность точечной инвалидации. Разделяйте по типу.
  • Отсутствие версионирования: если имя кэша не меняется между релизами, старые ассеты живут вечно и пожирают квоту.
  • Игнорирование request.mode === 'navigate': проверяя только destination, вы можете случайно закэшировать HTML как CSS.
  • Service Worker на staging без HTTPS: используйте ngrok, Cloudflare Tunnel или локальный сертификат, иначе регистрация молча провалится.
  • Отсутствие офлайн-фолбэка: даже если ваше приложение не PWA, страница /offline.html со ссылкой на кэшированный контент заметно улучшает UX при потере связи.

Ещё рекомендую посмотреть нашу статью про оптимизацию JavaScript-бандла в 2026. Маленький SW (≤20 КБ) переживает выселение лучше, поэтому tree-shaking стоит применить и к коду воркера. А Speculation Rules API работает в связке с Service Worker: пререндерные документы используют тот же SW, так что готовый кэш ускоряет prerender ещё сильнее.

Часто задаваемые вопросы

Чем Service Worker отличается от Web Worker?

Web Worker, это обычный фоновый поток для CPU-задач, привязан к открытой вкладке и не имеет доступа к сети как прокси. Service Worker живёт независимо от вкладок, перехватывает все сетевые запросы в своём scope через событие fetch и работает офлайн. Они решают разные задачи и могут использоваться вместе.

Что лучше использовать, Workbox или чистый Service Worker?

Workbox 7 предпочтительнее для проектов с PWA, Background Sync, кросс-браузерными нюансами и сложной инвалидацией: он экономит десятки часов и закрывает граничные случаи. Чистый Service Worker оправдан для лендингов и небольших сайтов, где нужен предсказуемый размер бандла (60 строк против 14 КБ ядра Workbox).

Замедляет ли Service Worker первую загрузку страницы?

На первом визите Service Worker НЕ участвует в обработке запросов, он только регистрируется и устанавливается в фоне после load. Замедление возможно на втором визите, если код в fetch-листенере медленный или нет navigationPreload. Современные браузеры дополнительно прогревают SW спекулятивно.

Как полностью удалить Service Worker?

На стороне клиента: navigator.serviceWorker.getRegistrations().then(rs => rs.forEach(r => r.unregister())) и затем caches.keys().then(ks => ks.forEach(k => caches.delete(k))). Чтобы пользователи получили эту команду, задеплойте «kill switch», версию SW, которая снимает себя с регистрации в событии install.

Поддерживается ли Service Worker в Safari в 2026?

Да, Safari поддерживает Service Worker и Cache API с версии 11.1 (2018). В 2026 году Safari 17+ на iOS поддерживает полный спектр PWA, включая push-уведомления для добавленных на главный экран приложений. Главное ограничение iOS, выселение Cache Storage после 7–30 дней неактивности.

Можно ли использовать Service Worker без PWA-манифеста?

Да, эти технологии независимы. Service Worker полезен сам по себе для офлайн-кэширования и ускорения повторных визитов; PWA-манифест (manifest.webmanifest) нужен только если вы хотите, чтобы сайт устанавливался как приложение. Многие крупные сайты используют SW без манифеста просто ради перф-выигрыша.

Editorial Team
Об авторе Editorial Team

Our team of expert writers and editors.