Service Worker — это тот случай, когда одна небольшая штука в браузере может заметно изменить поведение сайта. На деле он отвечает и за кэш, и за офлайн-режим, и за фоновые задачи, и за более быстрый повторный заход. Я обычно внедряю его там, где уже сделана нормальная база: HTTPS, адекватный кэш статики, понятная структура ассетов и без хаоса в фронтенде.
Что такое Service Worker и зачем он нужен сайту
Грубо говоря, Service Worker — это отдельный JavaScript-скрипт, который работает не в основном потоке страницы, а в фоне. Он перехватывает запросы браузера, может отдавать данные из кэша, может сохранять ответы для офлайна и умеет контролировать, как сайт ведёт себя при повторном открытии. Я обычно объясняю клиентам так: это не “ускоритель ради ускорения”, а инструмент контроля над загрузкой.
На моей практике Service Worker особенно полезен для контентных сайтов, интернет-магазинов и личных кабинетов, где пользователи регулярно возвращаются. Если человек открыл сайт второй раз, браузеру уже не надо тянуть с нуля весь набор картинок, CSS и JS. И это не просто приятный бонус. Это реально влияет на скорость первого взаимодействия, на стабильность в плохой сети и на восприятие сайта в целом.
Но есть нюанс. Service Worker — это не магия и не замена серверной оптимизации. Если у вас тяжёлый WordPress с десятком визуальных конструкторов, или Bitrix с кучей модулей и запросов к базе, то сначала надо убрать лишний вес. Про это я уже не раз писал в материале про ускорение WordPress и в статье про кеширование в Битрикс. Service Worker хорошо работает поверх нормального фундамента, а не вместо него.
Как работает кэширование в Service Worker
У Service Worker есть своя модель кэширования. Я обычно использую её в трёх вариантах: cache first, network first и stale-while-revalidate. Звучит сложно, но по опыту это вполне приземлённые сценарии.
Cache first — сначала отдаём из кэша, а в сеть идём только если файла там нет. Отлично подходит для статики: CSS, JS, шрифты, иконки, локальные изображения. Network first — сначала пытаемся получить свежий ответ с сервера, а если сеть не ответила, берём кэш. Это уже история для HTML-страниц, где важно показать актуальный контент. Stale-while-revalidate — пользователю отдаём кэш сразу, а в фоне обновляем его с сервера. Это очень практичный вариант для новостей, каталога и блога.
Честно говоря, вот здесь чаще всего и совершают ошибку: кэшируют вообще всё подряд. А потом удивляются, почему пользователь видит старую корзину, старый прайс или старую форму. Это плохая идея. Динамику, персональные данные, авторизованные ответы API и страницы с критичными обновлениями надо кэшировать с умом, а иногда вообще не кэшировать.
Если у вас проект на Laravel, WordPress или Bitrix, я обычно разделяю кэш на слои. Серверный кэш отдельно, CDN отдельно, браузерный кэш отдельно, Service Worker отдельно. Если смешать всё в кучу, потом очень трудно понять, где именно сломалось. Подробно про базовый слой ускорения у меня есть статья про кэширование статики и заголовки Cache-Control, и это хороший фундамент перед внедрением Service Worker.
Подготовка сайта перед внедрением
Перед тем как писать Service Worker, я всегда проверяю несколько вещей. Во-первых, сайт должен работать по HTTPS. Без этого браузер просто не даст зарегистрировать Service Worker. Если HTTPS ещё не настроен, сначала читаем про SSL-сертификат и при необходимости про HTTPS редиректы и HSTS. И да, HSTS очень желательно, если вы хотите, чтобы сайт открывался стабильно и без “прыжков” между протоколами.
Во-вторых, нужно понимать структуру ассетов. Где лежит CSS? Где JS? Какие изображения можно кэшировать, а какие нельзя? Есть ли API-запросы? Используется ли CDN? У одного клиента на WordPress с PHP 8.2 и MySQL 8.0 я видел ситуацию, когда фронтенд тащил и локальные ассеты, и файлы с стороннего домена, и всё это потом попадало в Service Worker без фильтрации. Итог — кэш разросся, а при обновлении темы часть файлов осталась старой. Пришлось чистить и переписывать стратегию.
В-третьих, надо заранее продумать версионирование. Я обычно добавляю версию в имя кэша, например site-cache-v3, а при деплое повышаю номер. Это проще, чем пытаться угадать, когда браузер обновит старый кэш. Особенно это важно, если вы релизите часто — например, раз в неделю или даже каждый день. Если у вас ещё и настроен CI/CD, посмотрите материал про Docker, Git и CI/CD. Там логика обновлений и версионирования очень хорошо стыкуется с Service Worker.
Базовая регистрация Service Worker
Самый простой Service Worker начинается с регистрации на стороне сайта. Я обычно кладу файл sw.js в корень сайта или в доступный статический каталог, чтобы он имел широкий scope. Если положить его в подпапку, он будет контролировать только эту папку или часть сайта, и это не всегда то, что нужно.
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js')
.then(function (registration) {
console.log('Service Worker зарегистрирован:', registration.scope);
})
.catch(function (error) {
console.error('Ошибка регистрации Service Worker:', error);
});
});
}
На практике я почти всегда добавляю регистрацию после load, а не сразу в DOMContentLoaded. Так меньше риск затормозить критическую отрисовку. Да, можно подключить и раньше, но смысла в этом обычно мало. Сначала пусть сайт покажет контент, потом уже догоняет Service Worker.
Сам файл sw.js можно начать с базового кэширования ресурсов при установке. Это самый простой сценарий pre-cache, когда браузер заранее сохраняет нужные файлы. Для статического сайта это может быть вообще 10–20 файлов: CSS, JS, логотип, несколько иконок, офлайн-страница. Для сайта на WordPress или Bitrix я обычно не кладу туда слишком много. Всё, что часто меняется, лучше загружать по другой стратегии.
const CACHE_NAME = 'site-cache-v1';
const ASSETS_TO_CACHE = [
'/',
'/offline.html',
'/assets/css/main.css',
'/assets/js/main.js',
'/assets/img/logo.svg'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS_TO_CACHE))
);
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => key !== CACHE_NAME ? caches.delete(key) : null)
))
);
self.clients.claim();
});
Вот тут я обычно делаю важную ремарку. skipWaiting() и clients.claim() удобны, но использовать их бездумно нельзя. Если у вас сложное приложение, резкий переход на новую версию Service Worker может повести себя неожиданно. У меня был случай на проекте с личным кабинетом: пользователь держал открытую вкладку, мы задеплоили новый sw, и интерфейс начал вести себя неодинаково в разных вкладках. После этого я стал намного аккуратнее с обновлениями и откатами. Кстати, про нормальную стратегию обновления и отката у меня есть отдельный разбор: версионирование и откат обновлений сайта.
Стратегии кэширования на практике
Если говорить без академизма, то в реальном проекте я обычно делю ресурсы так:
- HTML-страницы — network first или stale-while-revalidate.
- CSS/JS — cache first, но с версионированием.
- Изображения — cache first, особенно если это иконки, логотипы, баннеры.
- API-ответы — очень осторожно, чаще network first или вообще без кэша.
- Офлайн-страница — обязательна, если вы реально хотите офлайн-режим.
Я обычно начинаю с простого fetch-обработчика. Он перехватывает запросы и решает, откуда брать ответ. Ниже пример для статических файлов и HTML-страниц. Это не универсальная схема, но для большинства корпоративных сайтов работает нормально.
self.addEventListener('fetch', event => {
const { request } = event;
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then(response => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(request, responseClone));
return response;
})
.catch(() => caches.match('/offline.html'))
);
return;
}
event.respondWith(
caches.match(request).then(cachedResponse => {
return cachedResponse || fetch(request).then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(request, response.clone());
return response;
});
});
})
);
});
Но и тут есть тонкость. Если вы кэшируете HTML-страницы, то должны понимать, что это влияет на свежесть контента. У новостного сайта, где публикации выходят каждый час, я бы кэшировал HTML очень коротко или использовал stale-while-revalidate. Для каталога товаров — зависит от частоты обновления цен и наличия. Для лендинга — можно смело кэшировать дольше.
По опыту, лучше сначала измерить реальную динамику контента. Если контент обновляется раз в неделю, нет смысла городить сложнейшую схему. Если цены и остатки меняются каждые 15 минут, тогда Service Worker должен вести себя аккуратнее, а иногда даже обращаться к серверу чаще, чем хочется. Это особенно актуально, если вы уже используете Redis или Memcached. Об этом у меня есть отдельная статья про настройку Redis для сайта.
Офлайн-режим и страница без интернета
Офлайн-режим — это то, ради чего многие вообще и затевают Service Worker. Но тут есть важный момент: офлайн-режим не означает, что сайт будет полноценно работать без сети. Он означает, что вы сможете показать пользователю заранее подготовленный контент или хотя бы внятную страницу с сообщением, что интернет недоступен.
Я обычно делаю минимум две вещи. Первая — кэширую offline.html. Вторая — настраиваю fallback для навигационных запросов. То есть если пользователь переходит на страницу, а сети нет, браузер показывает офлайн-страницу вместо стандартной ошибки. Это уже гораздо лучше, чем белый экран или скучный ERR_INTERNET_DISCONNECTED.
Если проект сложнее, можно добавить ещё и офлайн-контент: последние просмотренные статьи, карточки товаров, список часто открываемых страниц. Но честно говоря, это уже не “просто Service Worker”, а полноценная UX-логика. На моей практике такие сценарии хорошо заходят в PWA-проектах, где офлайн — часть продукта, а не побочный бонус. Про это у меня есть отдельный материал: настройка PWA для сайта.
Важно ещё и правильно оформить саму офлайн-страницу. Не надо писать сухое “нет соединения”. Лучше объяснить, что человек может сделать дальше: попробовать обновить страницу, перейти в ранее сохранённый раздел, открыть страницу при восстановлении сети. Если это интернет-магазин, можно показать избранное или каталог, который был открыт раньше. Если это корпоративный сайт, пусть человек хотя бы увидит контакты и реквизиты.
Настройка на Nginx и .htaccess: что проверить на сервере
Service Worker — это фронтендовая часть, но сервер тоже должен быть настроен правильно. У одного клиента сайт работал на Nginx 1.24 и PHP 8.2, и проблема была банальная: сервер отдавал неправильные заголовки для части файлов, плюс были редиректы, которые мешали отладке. Поэтому я всегда проверяю, чтобы корневой sw.js отдавался без лишних редиректов и с корректным Content-Type.
Для Nginx обычно достаточно убедиться, что файл доступен и не ломается правилами cache-control. Примерно так:
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
location = /offline.html {
add_header Cache-Control "no-cache";
try_files $uri =404;
}
Я намеренно ставлю для sw.js запрет на агрессивное кэширование. Иначе браузер может слишком долго держать старую версию файла. Это одна из самых частых причин, почему обновления Service Worker “как будто не доезжают”. На самом деле они доезжают, просто браузер упорно использует старый скрипт.
Если сайт на Apache, то можно прописать это и через .htaccess:
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
Header set Cache-Control "no-cache"
И ещё момент, который часто забывают. Если у вас стоит CDN, проверьте его правила. Иногда CDN кэширует sw.js дольше, чем нужно, и тогда на стороне браузера всё выглядит как “сломанный Service Worker”. На деле виноват CDN, а не код. Про базовую настройку CDN у меня есть отдельная статья: как настроить CDN для сайта.
Типичные ошибки и как их исправить
Первая ошибка — кэшировать всё подряд. Я уже говорил об этом, но повторю ещё раз: это плохая идея. Особенно если сайт содержит личный кабинет, формы оплаты, актуальные цены, персональные сообщения или динамические фильтры. Там кэш надо выстраивать очень точечно.
Вторая ошибка — забывать про обновление кэша. Вы меняете файл main.js, а в браузере пользователя всё ещё лежит старая версия. Решается это версионированием файлов или кэша. Я обычно делаю и то, и другое. Например, main.3f2c1.js плюс site-cache-v4. Так обновления проходят предсказуемее.
Третья ошибка — не тестировать офлайн и плохую сеть. И вот тут я всегда советую запускать проверку в Chrome DevTools: вкладка Network, режим Offline, throttle Slow 3G. За 5 минут можно увидеть половину будущих проблем. Если хотите довести процесс до системы, посмотрите ещё статью про автоматическое тестирование сайта. Service Worker без тестов — это очень скользкая история.
Четвёртая ошибка — конфликт с другими кэшами. Например, на сайте уже стоит кеширующий плагин WordPress, серверный кеш Битрикс, Redis, CDN и ещё Service Worker. И если всё это не согласовано, пользователи начинают видеть странные артефакты. На одной из доработок я буквально потратил полдня только на то, чтобы понять, где живёт устаревший CSS: в браузере, в CDN или в Service Worker. После этого я всегда сначала строю карту кэшей, а потом уже внедряю новый слой. Если сайт уже страдает от накопленных проблем, полезно почитать и про технический долг сайта.
Тестирование и поддержка в реальных условиях
После внедрения Service Worker я всегда проверяю несколько сценариев: первый визит, повторный визит, обновление версии, очистка кэша, офлайн-режим, частичная потеря сети, работа на мобильном интернете. Особенно важно тестировать на телефоне, потому что у пользователей там чаще всего и возникают реальные проблемы: нестабильный 4G, экономия трафика, медленный DNS, разрывы соединения.
Если проект крупный, я ещё смотрю логи и аналитику. Например, сколько раз срабатывает fallback на офлайн-страницу, как часто старые версии Service Worker остаются активными, есть ли жалобы на “не обновляется сайт”. Для этого полезен мониторинг и uptime-контроль. У меня была история, когда после релиза на сайте всё выглядело нормально в офисной сети, но пользователи с мобильного интернета жаловались на пустую страницу товара. Проблема оказалась в том, что один JS-файл кэшировался неправильно, а fallback не ловил нужный сценарий. После правки статистика по жалобам упала почти до нуля.
И ещё. Service Worker надо сопровождать, как и любой код. Он не ставится “раз и навсегда”. Если вы обновляете дизайн, меняете структуру страниц, добавляете API, переносите сайт на другую CMS или даже просто обновляете шаблон — кэш-логику нужно пересматривать. Особенно это актуально после крупных изменений, о которых я подробно писал в материалах про редизайн сайта и перенос сайта на другую CMS.
Если говорить по опыту, Service Worker отлично подходит тем, кто хочет улучшить пользовательский опыт без тяжёлой переделки всего сайта. Но внедрять его надо спокойно и технично. Сначала HTTPS, потом кэш статики, затем fallback-страницы, потом офлайн-сценарии, и только после этого — более сложная логика. На сайте webfull.ru я обычно начинаю такие работы с аудита текущего состояния, потому что без этого легко наделать лишнего. Если нужен аккуратный разбор и внедрение под ваш проект, смотрите поддержку WordPress, поддержку Битрикс или доработку сайта.
Хотите настроить офлайн-режим на сайте?
Мы поможем внедрить Service Worker, настроить кэш и сделать сайт доступным даже без интернета.