Offline-first приложения

Offline-first — подход к разработке, при котором приложение работает без сети, а синхронизация с сервером происходит при восстановлении соединения.

Зачем нужно

Нестабильная сеть — реальность для мобильных пользователей. Offline-first приложения не «падают» без интернета: они читают из локального кэша, принимают действия пользователя и синхронизируют их позже. Это улучшает UX и позволяет работать в метро, самолёте и при слабом сигнале.

Где используется

  • PWA (Progressive Web Apps) — сохранение страниц для офлайн-доступа
  • Мобильные приложения (React Native, Capacitor) с локальной БД
  • Google Docs, Figma, Notion — редактирование без сети
  • Приложения для работы в полях (склад, медицина, логистика)

Ключевые инструменты

Service Worker + Cache API

// sw.js — сервис-воркер
const CACHE_NAME = 'v1';
const PRECACHE = ['/index.html', '/app.js', '/styles.css'];

// Установка: кэшируем статику
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE))
  );
});

// Запрос: сначала кэш, потом сеть (Cache First)
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;
      return fetch(event.request).then(response => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
        return response;
      });
    })
  );
});

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

Cache First     — отдаём кэш, сеть — только если нет (статика, изображения)
Network First   — пробуем сеть, при ошибке — кэш (динамические данные)
Stale-While-Revalidate — кэш сразу + обновляем фоном (баланс скорости и свежести)
Network Only    — только сеть (платёжные данные, критичная актуальность)
Cache Only      — только кэш (предзагруженные ресурсы)

IndexedDB для локальных данных

// Сохраняем данные локально при действии пользователя
async function saveTaskOffline(task) {
  const db = await openDB('myapp', 1, {
    upgrade(db) {
      db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
      db.createObjectStore('sync-queue', { keyPath: 'id', autoIncrement: true });
    },
  });

  await db.put('tasks', task);
  // Добавляем в очередь синхронизации
  await db.add('sync-queue', { action: 'create', data: task, timestamp: Date.now() });
}

Background Sync API

// Регистрируем фоновую синхронизацию
async function scheduleSync() {
  const reg = await navigator.serviceWorker.ready;
  await reg.sync.register('sync-tasks');
}

// В сервис-воркере — выполняем при восстановлении сети
self.addEventListener('sync', event => {
  if (event.tag === 'sync-tasks') {
    event.waitUntil(syncPendingTasks);
  }
});

async function syncPendingTasks() {
  const db = await openDB('myapp', 1);
  const queue = await db.getAll('sync-queue');

  for (const item of queue) {
    await fetch('/api/tasks', {
      method: 'POST',
      body: JSON.stringify(item.data),
      headers: { 'Content-Type': 'application/json' },
    });
    await db.delete('sync-queue', item.id);
  }
}

Частые ошибки

  • Кэшируют API-ответы с Cache First — пользователь видит устаревшие данные
  • Не обрабатывают конфликты синхронизации — два устройства изменили один объект
  • Хранят чувствительные данные в IndexedDB без шифрования
  • Не тестируют офлайн-режим — Chrome DevTools → Network → Offline

Связанные темы

Ресурсы