Если вам нужен быстрый сайт с мгновенной отдачей статики и работа «как приложение» даже при нестабильном интернете — начните с самой понятной схемы кеширования: 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)
- Контент-хеши в именах файлов (например, app.83f2c1.js). При сборке (Vite/Webpack/Rollup) включите хеширование.
- Версионирование кэша: меняйте CACHE_VERSION → v2 при релизе.
- 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: политика кэширования «из коробки», экспирация, версионирование, удобная интеграция с билдом.
Как проверить, что всё работает
- DevTools → Application → Service Workers: SW «активен», есть контролируемая страница.
- Application → Cache Storage: видите ваш static-v1, внутри — файлы.
- Симуляция офлайна (Network → Offline): страница открывается, статика грузится из кэша, изображения — либо из кэша, либо показывается offline.svg.
- Lighthouse: метрика Performance растёт, Best Practices/PWA — зелёные.
- Обновление версии: меняете 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.