Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке service worker?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Service worker** - це фоновий JavaScript-скрипт, який працює в окремому потоці, перехоплює мережеві запити і забезпечує офлайн-режим для вебзастосунків. ```javascript navigator.serviceWorker.register('/sw.js'); ``` **Головне:** без доступу до DOM, але з повним контролем над кешем і мережевими запитами. Основа Progressive Web Apps (PWA).Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 отримує свіжу версію і зберігає в кеш. Наступний відвід вже отримає оновлений контент. Хороший баланс між швидкістю та актуальністю для сторінок, що змінюються час від часу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.