Skip to main content

Що таке service worker?

Service worker - це JavaScript-скрипт, який працює у фоновому режимі у власному потоці, окремо від вебсторінки, і перехоплює кожен мережевий запит застосунку.

Теорія

TL;DR

  • Service worker стоїть між сторінкою і мережею, як проксі, яким ти керуєш через код
  • Працює у власному потоці, не має доступу до DOM, і продовжує жити після закриття сторінки
  • Браузер запускає три події по черзі: install (кешування ресурсів), activate (очищення старого кешу), fetch (перехоплення запитів)
  • Потрібен, коли є офлайн-режим, швидший повторний запуск, push-сповіщення або фонова синхронізація
  • Не потрібен для простих сайтів або серверних застосунків без офлайн-вимог

Швидкий приклад

javascript
// 1. Реєстрація в основному файлі застосунку if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered')) .catch(err => console.log('SW failed:', err)); } // 2. Всередині sw.js - повертаємо закешовану відповідь або йдемо в мережу self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) // Спочатку кеш, мережа - запасний варіант ); });

Два файли: реєстрація виконується на сторінці, логіка worker'а - у sw.js у власному потоці.

Окремий потік, без DOM

Service worker працює поза основним потоком. Він ніколи не блокує рендер UI, що б не робив. Але водночас у нього немає document, window, localStorage. Спроба звернутись до document всередині service worker дає ReferenceError.

Для передачі даних між worker'ом і сторінкою використовується postMessage(). Worker надсилає повідомлення, сторінка слухає і оновлює DOM. Це єдиний канал комунікації.

Життєвий цикл

Перш ніж почати обробляти запити, service worker проходить три фази:

  1. install - спрацьовує один раз, коли браузер вперше завантажує скрипт. Тут попередньо кешуємо ресурси через event.waitUntil(). Якщо кешування не вдалось - install провалюється, worker не активується.
  2. activate - спрацьовує після install, коли старий service worker вже неактивний. Тут очищаємо застарілі кеші, щоб користувачі не застрягали зі старими файлами.
  3. fetch - спрацьовує на кожен мережевий запит сторінки. Тут вирішуємо: повернути з кешу, піти в мережу, або поєднати обидва підходи.

Після активації service worker живе у фоні навіть після закриття сторінки. Саме це уможливлює push-сповіщення та фонову синхронізацію.

Коли використовувати

  • Progressive Web Apps (PWA): потрібен офлайн-режим або встановлення як застосунку
  • Продуктивність: кешування статики (CSS, JS, зображення) - повторні відвідини без звернення до мережі
  • Нестабільна мережа: мобільні користувачі, які регулярно втрачають з'єднання
  • Фонова синхронізація: черга форм в офлайні, відправка при відновленні з'єднання
  • Push-сповіщення: показ сповіщень навіть при закритій вкладці

Для простих сайтів, повністю серверних застосунків або проєктів без офлайн-вимог service worker не потрібен.

Стратегії кешування

Дві стратегії закривають більшість реальних потреб.

Cache-first для статики: спочатку перевіряємо кеш, повертаємо одразу, йдемо в мережу лише якщо немає. Швидко. Підходить для CSS, JS, шрифтів, які рідко змінюються.

Network-first для API: спочатку мережа, кешуємо відповідь, при офлайні беремо з кешу. Дані залишаються свіжими. Підходить для профілів користувачів, стрічок, будь-якого динамічного контенту.

Стратегію вибираємо для кожного типу запиту окремо. Один обробник fetch може застосовувати обидва підходи одночасно.

Типові помилки

1. Не версіонувати кеші

Браузер кешує сам скрипт service worker. Оновив /sw.js - користувачі отримають нову версію лише після закриття всіх вкладок. Рішення: версіонувати назви кешів при кожному деплої.

javascript
// Погано: назва кешу не змінюється, користувачі застрягають зі старим const CACHE_NAME = 'app-cache'; // Добре: змінюємо версію при кожному релізі const CACHE_NAME = 'app-v2'; self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(names => Promise.all(names.map(name => name !== CACHE_NAME && caches.delete(name) )) ) ); });

2. Кешувати API-відповіді без урахування застарілих даних

Закешували профіль користувача - він лежить у кеші, поки не видалиш явно. Сервер оновив дані - користувач бачить старі. Для всього, що змінюється на сервері, використовуй network-first. Cache-first - тільки для статики.

3. Звертатися до DOM

javascript
// Погано: ReferenceError всередині service worker self.addEventListener('fetch', event => { document.body.innerHTML = 'Offline'; // ReferenceError }); // Добре: надсилаємо повідомлення на сторінку self.clients.matchAll().then(clients => { clients.forEach(client => client.postMessage({ status: 'offline' })); });

4. Ігнорувати вимогу HTTPS

Service workers працюють тільки через HTTPS. Локально достатньо localhost або 127.0.0.1. Якщо задеплоїти на HTTP - service worker не зареєструється взагалі. Перш ніж щось дебажити, перевіряй протокол продакшн-середовища.

5. Очікувати миттєвого оновлення

Новий код service worker не активується, поки користувач не закриє всі відкриті вкладки. Якщо потрібна негайна активація, використовуй self.skipWaiting() в install і clients.claim() в activate. Але тільки якщо нова версія повністю сумісна з наявними кешованими даними.

Де зустрічається

  • Workbox (Google): бібліотека, яка обгортає стратегії кешування у простий API; використовується в Gatsby, Create React App, Firebase Hosting
  • Next.js: плагін next-pwa додає підтримку service worker для статичного експорту
  • Notion, Figma, Google Docs: кешують документи локально і синхронізують зміни при відновленні з'єднання
  • Twitter / X: ставить пости в чергу при офлайні, надсилає при відновленні
  • Shopify PWA: кешує сторінки товарів і оформлення замовлення для швидшого завантаження

Бачив, як команди пропускали оновлення версії кешу і витрачали пів дня на розбір того, чому частина користувачів бачить зламаний layout. Автоматизуй збільшення версії через CI/CD - і цієї проблеми не буде.

Питання на співбесіді

Q: Яка різниця між service worker і web worker?
A: Web worker працює у фоновому потоці, але прив'язаний до конкретної сторінки і зупиняється при її закритті. Service worker живе незалежно від сторінок, обробляє запити з кількох вкладок і перехоплює мережевий трафік. Web worker - для важких обчислень, service worker - для кешування і офлайну.

Q: Чи може service worker звертатись до localStorage?
A: Ні. localStorage доступний тільки в основному потоці. Замість нього використовуй IndexedDB - він доступний у service workers, підтримує більший обсяг даних (50MB+ проти 5-10MB) і спроектований для асинхронного доступу.

Q: Як оновити service worker без порушення роботи застосунку?
A: Версіонуй назви кешів, очищай старі кеші в activate і показуй банер "доступна нова версія" з перезавантаженням за кліком. skipWaiting() - тільки якщо впевнений у зворотній сумісності нової версії.

Q: Що станеться, якщо service worker впаде з помилкою?
A: Браузер перехопить помилку, worker зупиниться, запити підуть напряму в мережу. Реєстрація збережеться. Лог помилки буде у DevTools > Application > Service Workers.

Q: (Senior) Як обробляти конфлікти, якщо користувач редагував дані в офлайні кілька годин, а на сервері за цей час теж були зміни?
A: Зберігати кожну офлайн-зміну в IndexedDB з timestamp і ID зміни. При відновленні з'єднання порівнюємо локальні зміни з серверною версією і застосовуємо стратегію вирішення конфліктів. Last-write-wins простий, але деструктивний. Злиття на рівні полів складніше, але безпечніше. Бібліотеки Automerge і Yjs реалізують CRDT (структури даних без конфліктів репліки) і вирішують конфлікти автоматично. Найскладніше - відобразити результат злиття в UI так, щоб не збентежити користувача.

Приклади

Базовий: реєстрація та кешування статики

javascript
// В основному файлі застосунку if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } // sw.js const CACHE = 'app-v1'; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE).then(cache => cache.addAll(['/index.html', '/styles.css', '/app.js']) ) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(cached => cached || fetch(event.request)) ); });

Статичні ресурси кешуємо при install. При кожному запиті спочатку перевіряємо кеш, при відсутності йдемо в мережу.

Середній: cache-first для статики, network-first для API

javascript
const CACHE_NAME = 'app-v2'; self.addEventListener('fetch', event => { const { request } = event; // Cache-first: статичні файли завантажуються миттєво при повторному відвідуванні if (request.url.includes('/static/')) { event.respondWith( caches.match(request).then(cached => cached || fetch(request)) ); return; } // Network-first: API-відповіді залишаються свіжими, кеш - резервний при офлайні if (request.url.includes('/api/')) { event.respondWith( fetch(request) .then(response => { const copy = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(request, copy)); return response; }) .catch(() => caches.match(request)) ); } });

Один обробник fetch, дві стратегії. Статика не чекає мережі. API-виклики завжди намагаються отримати свіжі дані, але витримують офлайн.

Просунутий: stale-while-revalidate

javascript
// Повертаємо закешовану відповідь одразу, оновлюємо кеш у фоні self.addEventListener('fetch', event => { if (event.request.method !== 'GET') return; event.respondWith( caches.open(CACHE_NAME).then(cache => { return cache.match(event.request).then(cached => { const networkFetch = fetch(event.request).then(response => { if (response.status === 200) { cache.put(event.request, response.clone()); } return response; }); // Закешований контент одразу, свіжа версія зберігається у фоні return cached || networkFetch; }); }) ); });

Користувач бачить контент миттєво. У фоні service worker отримує свіжу версію і зберігає в кеш. Наступний відвід вже отримає оновлений контент. Хороший баланс між швидкістю та актуальністю для сторінок, що змінюються час від часу.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?