← Браузер/Service Workers: офлайн и кэширование#186 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

Service Workers: офлайн и кэширование

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

Жизненный цикл Service Worker

Service Worker проходит три фазы:

1. Регистрация

// Главный поток
if ('serviceWorker' in navigator) {
  const reg = await navigator.serviceWorker.register('/sw.js')
  console.log('SW зарегистрирован:', reg.scope)
}

2. Установка (install)

Срабатывает один раз при первой регистрации или при обновлении файла sw.js. Здесь заполняется кэш:

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then(cache => cache.addAll(['/index.html', '/app.js']))
  )
})

3. Активация (activate)

Срабатывает когда SW берёт управление. Здесь удаляются старые кэши:

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

Перехват запросов: fetch event

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request)
    })
  )
})

Стратегии кэширования

Cache First — сначала кэш, потом сеть. Максимальная скорость, подходит для статики (CSS, JS, шрифты).

Network First — сначала сеть, при ошибке — кэш. Подходит для API с актуальными данными.

Stale While Revalidate — отдаём из кэша немедленно, параллельно обновляем кэш из сети. Баланс скорости и свежести данных.

Cache Only — только кэш, никакой сети. Полностью офлайн-контент.

Network Only — только сеть, без кэширования. Для аналитики, платёжных запросов.

Background Sync

Background Sync позволяет откладывать действия до восстановления соединения. Пользователь отправил форму офлайн — данные сохранятся и уйдут на сервер, когда появится интернет:

// Главный поток — регистрируем синхронизацию
const reg = await navigator.serviceWorker.ready
await reg.sync.register('send-message')

// SW — выполняем при восстановлении сети
self.addEventListener('sync', (event) => {
  if (event.tag === 'send-message') {
    event.waitUntil(sendQueuedMessages())
  }
})

Push-уведомления

Service Worker может получать push-уведомления с сервера даже когда страница закрыта. Сервер отправляет push через Web Push Protocol на endpoint, полученный от браузера при подписке пользователя.

Инструменты разработки

В Chrome DevTools → Application → Service Workers можно: видеть статус SW, принудительно обновить, симулировать офлайн. Вкладка Cache Storage показывает всё закэшированное.

Важные ограничения

  • SW работает только на HTTPS (или localhost)
  • SW не имеет доступа к DOM
  • SW имеет собственный жизненный цикл — не всегда активен
  • Обновление SW происходит не мгновенно (нужно закрыть все вкладки)
  • Примеры

    Симуляция Service Worker с перехватом fetch и стратегией cache-first

    // Симулируем Cache API через Map
    class SimulatedCache {
      constructor() { this._store = new Map() }
    
      async put(url, response) {
        this._store.set(url, { ...response, cachedAt: Date.now() })
        console.log(`[Cache] Сохранено: ${url}`)
      }
    
      async match(url) {
        return this._store.get(url) || null
      }
    
      async addAll(urls) {
        for (const url of urls) {
          // Симулируем предзагрузку ресурсов
          await this.put(url, { status: 200, body: `content of ${url}` })
        }
      }
    
      get size() { return this._store.size }
      keys() { return [...this._store.keys()] }
    }
    
    // Симулируем сеть
    async function simulatedNetwork(url) {
      if (url.includes('offline')) throw new Error('Network error')
      return { status: 200, body: `fresh content from network: ${url}` }
    }
    
    // Сам Service Worker симулятор
    class ServiceWorkerSimulator {
      constructor() {
        this._cache = new SimulatedCache()
        this._strategy = 'cache-first'
        this._installed = false
        this._active = false
      }
    
      async install(assets) {
        console.log('[SW] Установка...')
        await this._cache.addAll(assets)
        this._installed = true
        console.log(`[SW] Установлен. Закэшировано ресурсов: ${this._cache.size}`)
      }
    
      async activate() {
        if (!this._installed) throw new Error('SW не установлен')
        this._active = true
        console.log('[SW] Активирован, контролирует страницу')
      }
    
      // Cache-First стратегия
      async fetch(url) {
        if (!this._active) {
          return simulatedNetwork(url)
        }
    
        if (this._strategy === 'cache-first') {
          const cached = await this._cache.match(url)
          if (cached) {
            console.log(`[SW] Из кэша: ${url}`)
            return cached
          }
          console.log(`[SW] Кэш пуст, запрос в сеть: ${url}`)
          try {
            const response = await simulatedNetwork(url)
            await this._cache.put(url, response)  // сохраняем для следующего раза
            return response
          } catch (err) {
            return { status: 503, body: 'Офлайн и нет в кэше' }
          }
        }
    
        if (this._strategy === 'network-first') {
          try {
            const response = await simulatedNetwork(url)
            await this._cache.put(url, response)
            console.log(`[SW] Из сети (обновлён кэш): ${url}`)
            return response
          } catch {
            const cached = await this._cache.match(url)
            if (cached) {
              console.log(`[SW] Сеть недоступна, из кэша: ${url}`)
              return cached
            }
            return { status: 503, body: 'Офлайн' }
          }
        }
      }
    }
    
    // Демо
    const sw = new ServiceWorkerSimulator()
    
    await sw.install(['/index.html', '/app.js', '/styles.css'])
    await sw.activate()
    
    console.log('\n--- Cache-First запросы ---')
    const r1 = await sw.fetch('/index.html')      // из кэша
    console.log('Ответ:', r1.body)
    
    const r2 = await sw.fetch('/api/data')        // не в кэше → сеть → кэш
    console.log('Ответ:', r2.body)
    
    const r3 = await sw.fetch('/api/data')        // теперь из кэша
    console.log('Ответ:', r3.body)
    
    console.log('\n--- Офлайн сценарий ---')
    sw._strategy = 'network-first'
    const r4 = await sw.fetch('/offline-resource')  // сеть упала, кэш пуст
    console.log('Ответ:', r4.body)

    Service Workers: офлайн и кэширование

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

    Жизненный цикл Service Worker

    Service Worker проходит три фазы:

    1. Регистрация

    // Главный поток
    if ('serviceWorker' in navigator) {
      const reg = await navigator.serviceWorker.register('/sw.js')
      console.log('SW зарегистрирован:', reg.scope)
    }

    2. Установка (install)

    Срабатывает один раз при первой регистрации или при обновлении файла sw.js. Здесь заполняется кэш:

    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open('v1').then(cache => cache.addAll(['/index.html', '/app.js']))
      )
    })

    3. Активация (activate)

    Срабатывает когда SW берёт управление. Здесь удаляются старые кэши:

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

    Перехват запросов: fetch event

    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request).then(cached => {
          return cached || fetch(event.request)
        })
      )
    })

    Стратегии кэширования

    Cache First — сначала кэш, потом сеть. Максимальная скорость, подходит для статики (CSS, JS, шрифты).

    Network First — сначала сеть, при ошибке — кэш. Подходит для API с актуальными данными.

    Stale While Revalidate — отдаём из кэша немедленно, параллельно обновляем кэш из сети. Баланс скорости и свежести данных.

    Cache Only — только кэш, никакой сети. Полностью офлайн-контент.

    Network Only — только сеть, без кэширования. Для аналитики, платёжных запросов.

    Background Sync

    Background Sync позволяет откладывать действия до восстановления соединения. Пользователь отправил форму офлайн — данные сохранятся и уйдут на сервер, когда появится интернет:

    // Главный поток — регистрируем синхронизацию
    const reg = await navigator.serviceWorker.ready
    await reg.sync.register('send-message')
    
    // SW — выполняем при восстановлении сети
    self.addEventListener('sync', (event) => {
      if (event.tag === 'send-message') {
        event.waitUntil(sendQueuedMessages())
      }
    })

    Push-уведомления

    Service Worker может получать push-уведомления с сервера даже когда страница закрыта. Сервер отправляет push через Web Push Protocol на endpoint, полученный от браузера при подписке пользователя.

    Инструменты разработки

    В Chrome DevTools → Application → Service Workers можно: видеть статус SW, принудительно обновить, симулировать офлайн. Вкладка Cache Storage показывает всё закэшированное.

    Важные ограничения

  • SW работает только на HTTPS (или localhost)
  • SW не имеет доступа к DOM
  • SW имеет собственный жизненный цикл — не всегда активен
  • Обновление SW происходит не мгновенно (нужно закрыть все вкладки)
  • Примеры

    Симуляция Service Worker с перехватом fetch и стратегией cache-first

    // Симулируем Cache API через Map
    class SimulatedCache {
      constructor() { this._store = new Map() }
    
      async put(url, response) {
        this._store.set(url, { ...response, cachedAt: Date.now() })
        console.log(`[Cache] Сохранено: ${url}`)
      }
    
      async match(url) {
        return this._store.get(url) || null
      }
    
      async addAll(urls) {
        for (const url of urls) {
          // Симулируем предзагрузку ресурсов
          await this.put(url, { status: 200, body: `content of ${url}` })
        }
      }
    
      get size() { return this._store.size }
      keys() { return [...this._store.keys()] }
    }
    
    // Симулируем сеть
    async function simulatedNetwork(url) {
      if (url.includes('offline')) throw new Error('Network error')
      return { status: 200, body: `fresh content from network: ${url}` }
    }
    
    // Сам Service Worker симулятор
    class ServiceWorkerSimulator {
      constructor() {
        this._cache = new SimulatedCache()
        this._strategy = 'cache-first'
        this._installed = false
        this._active = false
      }
    
      async install(assets) {
        console.log('[SW] Установка...')
        await this._cache.addAll(assets)
        this._installed = true
        console.log(`[SW] Установлен. Закэшировано ресурсов: ${this._cache.size}`)
      }
    
      async activate() {
        if (!this._installed) throw new Error('SW не установлен')
        this._active = true
        console.log('[SW] Активирован, контролирует страницу')
      }
    
      // Cache-First стратегия
      async fetch(url) {
        if (!this._active) {
          return simulatedNetwork(url)
        }
    
        if (this._strategy === 'cache-first') {
          const cached = await this._cache.match(url)
          if (cached) {
            console.log(`[SW] Из кэша: ${url}`)
            return cached
          }
          console.log(`[SW] Кэш пуст, запрос в сеть: ${url}`)
          try {
            const response = await simulatedNetwork(url)
            await this._cache.put(url, response)  // сохраняем для следующего раза
            return response
          } catch (err) {
            return { status: 503, body: 'Офлайн и нет в кэше' }
          }
        }
    
        if (this._strategy === 'network-first') {
          try {
            const response = await simulatedNetwork(url)
            await this._cache.put(url, response)
            console.log(`[SW] Из сети (обновлён кэш): ${url}`)
            return response
          } catch {
            const cached = await this._cache.match(url)
            if (cached) {
              console.log(`[SW] Сеть недоступна, из кэша: ${url}`)
              return cached
            }
            return { status: 503, body: 'Офлайн' }
          }
        }
      }
    }
    
    // Демо
    const sw = new ServiceWorkerSimulator()
    
    await sw.install(['/index.html', '/app.js', '/styles.css'])
    await sw.activate()
    
    console.log('\n--- Cache-First запросы ---')
    const r1 = await sw.fetch('/index.html')      // из кэша
    console.log('Ответ:', r1.body)
    
    const r2 = await sw.fetch('/api/data')        // не в кэше → сеть → кэш
    console.log('Ответ:', r2.body)
    
    const r3 = await sw.fetch('/api/data')        // теперь из кэша
    console.log('Ответ:', r3.body)
    
    console.log('\n--- Офлайн сценарий ---')
    sw._strategy = 'network-first'
    const r4 = await sw.fetch('/offline-resource')  // сеть упала, кэш пуст
    console.log('Ответ:', r4.body)

    Задание

    Реализуй createServiceWorkerSimulator() — объект с тремя методами: install(assets) принимает массив URL и «кэширует» их (сохраняет в Map); fetch(url) реализует стратегию cache-first: сначала ищет в кэше, при отсутствии возвращает `{ status: 200, body: "network: " + url }` и сохраняет в кэш; getCache() возвращает массив закэшированных URL.

    Подсказка

    Используй Map для хранения кэша. install() сохраняет каждый URL как ключ со значением { status: 200, body: "cached: " + url }. fetch() проверяет cache.has(url) и возвращает cache.get(url) при попадании. getCache() возвращает [...cache.keys()].

    Загружаем среду выполнения...
    Загружаем AI-помощника...