Найти в Дзене

Service Worker: простая стратегия Cache First

Если вам нужен быстрый сайт с мгновенной отдачей статики и работа «как приложение» даже при нестабильном интернете — начните с самой понятной схемы кеширования: Cache First. В этой статье — что это такое, когда уместно применять, готовый sw.js, тонкости, проверки скоростью и чек-лист внедрения. Cache First = «сначала кэш, потом сеть».
Браузер перехватывает запрос сервис-воркером и пытается отдать ресурс из Cache Storage. Если в кэше нет — тянет из сети, кладёт в кэш на будущее и возвращает пользователю. Где уместно: Где лучше не применять (или применять осторожно): Добавьте в HTML/главный бандл: <script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.catch(console.error);
});
}
</script> Положите рядом простой /offline.html и, при желании, заглушку картинки /offline.svg. /* sw.js */
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PRECACHE_URLS = [
Оглавление

Если вам нужен быстрый сайт с мгновенной отдачей статики и работа «как приложение» даже при нестабильном интернете — начните с самой понятной схемы кеширования: Cache First. В этой статье — что это такое, когда уместно применять, готовый sw.js, тонкости, проверки скоростью и чек-лист внедрения.

Что такое Cache First

Cache First = «сначала кэш, потом сеть».

Браузер перехватывает запрос сервис-воркером и пытается отдать ресурс из Cache Storage. Если в кэше нет — тянет из сети, кладёт в кэш на будущее и возвращает пользователю.

Где уместно:

  • статические файлы: CSS, JS, шрифты, иконки, изображения;
  • инвариантные JSON-справочники (редко меняются);
  • страницы/чанк-бандлы с контент-хешами в имени (cache busting).

Где лучше не применять (или применять осторожно):

  • HTML-навигации (часто — Network First/Stale-While-Revalidate);
  • динамические API и персональные данные;
  • всё, что обязано быть свежим при каждом заходе.

Регистрация Service Worker

Добавьте в HTML/главный бандл:

<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.catch(console.error);
});
}
</script>

Готовый sw.js с Cache First и офлайн-фолбэком

Положите рядом простой /offline.html и, при желании, заглушку картинки /offline.svg.

/* sw.js */
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PRECACHE_URLS = [
'/', // если у вас SPA с shell
'/offline.html',
'/styles.css',
'/app.js',
'/logo.svg'
];

// На установке — холодный предкеш
self.addEventListener('install', event => {
event.waitUntil((async () => {
const cache = await caches.open(STATIC_CACHE);
// cache:'reload' гарантирует скачивание с сети, а не из HTTP-кеша
await cache.addAll(PRECACHE_URLS.map(
u => new Request(u, { cache: 'reload' })
));
self.skipWaiting();
})());
});

// Активация — чистим старые версии
self.addEventListener('activate', event => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys.filter(k => k !== STATIC_CACHE).map(k => caches.delete(k))
);
// Мгновенно берём управление открытыми вкладками
await self.clients.claim();
})());
});

// Универсальный Cache First для GET-запросов
self.addEventListener('fetch', event => {
const req = event.request;
if (req.method !== 'GET') return; // не трогаем POST/PUT и т.д.

const url = new URL(req.url);

// Игнорируем сторонние схемы
if (url.protocol !== 'http:' && url.protocol !== 'https:') return;

// Для HTML-навигации: Cache First с офлайн-страницей
if (req.mode === 'navigate') {
event.respondWith((async () => {
// 1) кеш; 2) сеть; 3) офлайн-фолбэк
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match('/'); // или '/index.html' в MPA
try {
const fresh = await fetch(req, { priority: 'high' });
// Если у вас SPA-shell, можно прокешировать shell/HTML:
if (fresh && fresh.ok) cache.put('/', fresh.clone());
return fresh;
} catch (e) {
return cached || cache.match('/offline.html');
}
})());
return;
}

// Для статики: чистый Cache First
event.respondWith((async () => {
const cached = await caches.match(req, { ignoreSearch: false });
if (cached) return cached;

try {
const res = await fetch(req);
// Кешируем только успешные ответы
if (res && (res.ok || res.type === 'opaque')) {
const cache = await caches.open(STATIC_CACHE);
cache.put(req, res.clone());
}
return res;
} catch (e) {
// Фолбэк для изображений — опционально
if (req.destination === 'image') {
const cache = await caches.open(STATIC_CACHE);
const placeholder = await cache.match('/offline.svg');
if (placeholder) return placeholder;
}
// Иначе просто пробрасываем ошибку
throw e;
}
})());
});

Пояснения к коду

  • PRECACHE_URLS — список ресурсов, которые гарантированно нужны офлайн.
  • Очистка старых кэшей в activate предотвращает «разрастание» хранилища.
  • res.type === 'opaque' позволяет кешировать CORS-ресурсы без заголовков — аккуратно, они непрозрачны для логики проверки.
  • HTML навигации: показан комбинированный подход — пробуем сеть (чтобы не застаиваться), но в офлайне даём кеш/offline.html.

Как обновлять кэш (cache busting)

  1. Контент-хеши в именах файлов (например, app.83f2c1.js). При сборке (Vite/Webpack/Rollup) включите хеширование.
  2. Версионирование кэша: меняйте CACHE_VERSION → v2 при релизе.
  3. addAll(new Request(url, { cache: 'reload' })) — принудительная загрузка свежего контента при установке SW.

Паттерны и подводные камни

  • Шрифты и CORS: храните шрифты на том же домене или отдавайте с корректными CORS-заголовками.
  • POST/PUT нельзя кэшировать: используйте Background Sync/Retry-логики отдельно, если нужно.
  • Видео и range-запросы: простая реализация Cache First не поддерживает Range. Для стриминга лучше оставить сеть или перейти на Workbox с плагинами.
  • Персональные данные: не кэшируйте приватные responses; добавляйте условные фильтры по URL/заголовкам.
  • Размер кэша: периодически чистите. Проще — версионирование + Workbox Expiration (см. ниже).

Вариант на Workbox (ещё проще)

Если не хотите писать велосипеды — используйте Workbox:

// workbox-sw.js (генерится сборщиком)
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

registerRoute(
({request}) => ['style','script','font','image'].includes(request.destination),
new CacheFirst({
cacheName: 'static-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 дней
})
]
})
);

Плюсы Workbox: политика кэширования «из коробки», экспирация, версионирование, удобная интеграция с билдом.

Как проверить, что всё работает

  1. DevTools → Application → Service Workers: SW «активен», есть контролируемая страница.
  2. Application → Cache Storage: видите ваш static-v1, внутри — файлы.
  3. Симуляция офлайна (Network → Offline): страница открывается, статика грузится из кэша, изображения — либо из кэша, либо показывается offline.svg.
  4. Lighthouse: метрика Performance растёт, Best Practices/PWA — зелёные.
  5. Обновление версии: меняете CACHE_VERSION → перезагрузка → старые кэши удалились.

Типичная структура проекта

/public
/offline.html
/logo.svg
/sw.js
/assets
app.83f2c1.js
styles.1a2b3c.css
index.html

Мини-FAQ

Нужно ли кэшировать HTML?

Для SPA-shell — да, но осторожно: чаще используют Network First или Stale-While-Revalidate, чтобы не «залипать» на старой версии.

Что делать с динамическим API?

Для API лучше Network First, Cache Only на короткое время или вообще без SW, плюс ETag/Last-Modified на бэкенде.

Как отключить кэш для конкретного URL?

В fetch-обработчике добавьте фильтр по url.pathname и просто return fetch(request).

Чек-лист внедрения Cache First

  • Добавлен sw.js, регистрация в приложении.
  • Предкеш: shell/критическая статика/офлайн-страница.
  • Очистка старых кэшей при активировании.
  • Cache First для статики, офлайн-фолбэк для навигации и изображений.
  • Версионирование кэша и контент-хеши файлов.
  • Исключения: API/приватные данные не кэшируем.
  • Проверено в DevTools/Lighthouse и в офлайне.

Итог

Cache First — идеальная отправная точка для ускорения фронтенда: минимум кода, максимум эффекта для статики и офлайна. Начните с базового sw.js, добавьте офлайн-фолбэк и версионирование, а дальше при необходимости подключайте Workbox и более тонкие стратегии для HTML и API.