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