Service worker — это JavaScript-файл, который живёт отдельно от страниц сайта и выполняется в отдельном потоке браузера. Он не имеет доступа к DOM, не видит того, что показывается пользователю, не может ничего нарисовать. Но он умеет другое: перехватывать все сетевые запросы, идущие со страницы, и решать, что с ними делать — отдать из кэша, сходить в сеть, скомбинировать, заменить на оффлайн-заглушку.
Из-за этой возможности service worker — центральная часть PWA. Именно он отвечает за работу офлайн, push-уведомления и фоновые задачи.
Как зарегистрировать
Сам файл — обычный .js-файл, который лежит на сервере. На странице сайта его нужно зарегистрировать:
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW зарегистрирован, scope:", registration.scope);
})
.catch((err) => {
console.warn("Ошибка регистрации SW:", err);
});
});
}
Три важных момента про эту регистрацию.
Проверка поддержки. "serviceWorker" in navigator — страховка от старых браузеров, в которых API нет. На современных это всегда true.
Регистрация после load. Если зарегистрировать SW прямо в начале <script>, браузер начнёт качать sw.js параллельно с первичной загрузкой страницы. На медленном соединении это заметно замедлит первый рендер. Поэтому регистрацию вешают на событие load — страница уже отрисована, можно подгружать инфраструктуру.
Синтаксические ошибки = отказ. Если в sw.js есть синтаксическая ошибка или промис в install бросает исключение, регистрация целиком откатывается. SW даже не доходит до состояния «активен». Это значит, что любой try/catch вокруг сетевых вызовов внутри service worker — не паранойя, а необходимость.
HTTPS обязателен
Браузер откажется регистрировать service worker, если страница загружена не по HTTPS. Единственное исключение — localhost (для разработки) и 127.0.0.1. На любом другом домене — даже на staging-окружении — HTTPS обязателен.
Это решение из соображений безопасности. Service worker, проникший на сайт через MitM-атаку, мог бы перехватывать всё, что страница загружает с сервера. Поэтому пускают только проверенные источники.
Scope: что именно SW контролирует
У зарегистрированного service worker есть scope — путь, на котором он перехватывает запросы. По умолчанию scope равен директории, где лежит сам файл SW.
- sw.js в корне /sw.js — scope /, перехватывает запросы на весь сайт.
- sw.js в /admin/sw.js — scope /admin/, перехватывает только запросы к админке.
Расширить scope «вверх» нельзя. Файл в /admin/sw.js не получится сделать контроллером всего сайта — браузер запретит. Это защита от ситуации, когда подрядчик с правом загружать файлы только в /uploads/ мог бы перехватить весь сайт через SW.
Сузить scope при регистрации можно опцией:
navigator.serviceWorker.register("/sw.js", { scope: "/app/" });
Расширить за пределы расположения файла — нельзя, даже если очень хочется.
Жизненный цикл
Service worker проходит через три события: install, activate, fetch. Понимать порядок — критично, потому что многие странности с «обновлением, которое не подхватывается» объясняются именно ими.
install — одноразовое событие
Срабатывает один раз для каждой версии SW — сразу после регистрации. Хорошее место, чтобы предзагрузить в кэш статику, без которой приложение не запустится:
const CACHE_NAME = "pwa-static-v1";
const PRECACHE = [
"/",
"/index.html",
"/css/app.css",
"/js/app.js",
"/offline.html"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE))
);
});
Вызов event.waitUntil() говорит браузеру: «не считай установку завершённой, пока этот промис не разрешится». Если какой-то из URL в addAll не загрузится — вся установка откатится.
activate — SW взял управление
Срабатывает после успешной установки и после того, как все старые версии SW отпустили вкладки. По умолчанию старая версия SW работает до тех пор, пока все вкладки сайта не будут закрыты — именно поэтому обновления «не подхватываются» до перезагрузки.
В activate обычно чистят устаревшие версии кэша:
const CACHE_NAME = "pwa-static-v2";
self.addEventListener("activate", (event) => {
const validCaches = [CACHE_NAME];
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => !validCaches.includes(key))
.map((key) => caches.delete(key))
);
})
);
});
Если поднять версию кэша с pwa-static-v1 до pwa-static-v2 — старый кэш будет автоматически удалён при следующей активации. Без этого старые файлы будут копиться на диске пользователя бесконечно.
fetch — перехват сетевых запросов
Срабатывает на каждый запрос, который идёт со страницы в зоне scope. В обработчике вы решаете, что отдать в ответ.
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached;
}
return fetch(event.request).then((networkResponse) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
}).catch(() => caches.match("/offline.html"))
);
});
Логика этого примера:
- Сначала проверяем кэш — если файл там, отдаём его.
- Если нет — идём в сеть.
- Получив ответ, кладём копию в кэш для следующих запросов.
- Если сети нет совсем — отдаём offline.html.
Вызов networkResponse.clone() нужен, потому что ответ — стрим, его нельзя «прочитать» дважды. Одну копию кладём в кэш, оригинал возвращаем странице.
Это самая распространённая стратегия — Cache First. В следующей главе разберём её и ещё четыре варианта.
Принудительное обновление
Иногда нужно, чтобы новая версия SW взяла управление немедленно, не дожидаясь закрытия всех вкладок. Для этого — два метода:
self.addEventListener("install", (event) => {
self.skipWaiting();
// ...
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
// ...
});
skipWaiting() заставляет новую версию SW пропустить состояние waiting и сразу перейти к активации. clients.claim() — чтобы новый SW сразу начал контролировать все открытые вкладки. Использовать осторожно: если в одной вкладке у пользователя сейчас старая версия скриптов, она резко начнёт получать ответы от нового SW, и это может сломать состояние интерфейса.
На сегодня service worker поддерживается всеми актуальными десктопными и мобильными браузерами, включая Safari на iOS. Доля совместимых пользователей — больше 95%.