Оффлайн это старая мечта веба: страница, которая открывается в самолёте, в лифте, в деревне, где сеть ловит по вторникам. Первую серьёзную попытку воплотить мечту сделали во времена HTML5: Application Cache, коротко AppCache, текстовый манифест со списком файлов, который браузер терпеливо скачивал и держал у себя. Попытка провалилась громко, механизм вынесли с платформы, но следы живы до сих пор: в старых интранет-приложениях и заброшенных PWA атрибут манифеста продолжает гореть в разметке, а серверы послушно раздают файл, который браузеры давно перестали читать. Вопрос переноса с отжившего механизма на живой висит в бэклогах не один год, и закрывает его именно Service Worker.
Наследником стал Service Worker, и разница между двумя механизмами не в удобстве, а в философии: у AppCache правила зашиты в декларацию, у воркера их пишет автор в коде. Дальше оба механизма разобраны честно: как работал AppCache и за что его сняли с рельсов, как устроен воркер с Cache API, какие стратегии кэширования применять по типам ресурсов и как перевести старый проект на новые рельсы, не потеряв ни одного пользователя.
AppCache и его манифест как первая попытка оффлайн и почему её свернули
Механика была простой до трогательности. Атрибут manifest в корневом теге html указывал на текстовый файл с тремя секциями: CACHE перечисляла файлы для хранения, NETWORK помечала то, что всегда должно ходить в сеть, FALLBACK описывала подмены для недоступных страниц. При первом визите браузер скачивал всё из списка, а при следующих открывал из хранилища молча, без единого вопроса. Вот за это молчание и выпало всё.
Соблазн был настоящим: ни строчки кода, один текстовый файл, и страница уже работает из хранилища. Декларативность манила учебники и первые годы честно занимала своё место в списках возможностей HTML5 рядом с canvas и веб-хранилищем, пока практика не показала, что файл без логики не справляется с живым приложением.
Обновления работали как в плохом анекдоте. Менялась хоть одна строка манифеста, браузер перекачивал весь список целиком, новая версия включалась только после перезагрузки страницы, и пользователь продолжал работать на старой. Контроля в рантайме не было никакого: нельзя было понять, почему файл не обновился, нельзя было почистить хранилище из кода, нельзя было собрать кэш из разных источников. Смешанные версии встречались постоянно: разметка свежая, скрипт старый, и наоборот. Знаменитая разгромная статья 2012 года разложила механизм по полочкам и стала приговором: спецификацию признали тупиком, поддержку начали выпиливать, и в 2021 году, начиная с 93-й версии, Chrome окончательно распрощался с AppCache. В легаси разметке манифесты остались, но читает их теперь разве что историк.
CACHE MANIFEST
# v2.3
CACHE:
/app.js
/styles.css
/offline.html
NETWORK:
*
FALLBACK:
/ /offline.html
Service Worker и Cache API как управляемая смена парадигмы оффлайн хранения
Воркер это отдельный скрипт со своим жизненным циклом, без доступа к DOM, который браузер ставит между страницей и сетью. Каждый запрос страницы проходит через обработчик события fetch, и код решает его судьбу: отдать из хранилища, пойти в сеть, отдать из кэша и обновить в фоне, показать офлайн страницу. Хранилищем заведует Cache API: именованные кэши открываются через caches.open, стартовый набор грузится атомарно через addAll, точечная запись идёт через put, поиск через match.
Анатомия обработчика fetch проста: событие несёт запрос, метод respondWith принимает обещание ответа, и внутри можно сколько угодно ветвлений по типу запроса, его заголовкам и поисковой строке. Навигационные запросы документов, запросы стилей и скриптов, картинки и медиа живут по разным правилам, и именно поэтому воркер справляется там, где манифест складывал всё в одну кучу. Доступа к DOM у воркера нет, и это правильно: его ремесло это сеть и хранилище, а не разметка.
В отличие от отжившего предка воркер это код, и правила в нём пишет автор: версии хранилищ, приоритеты, подмены, уборку мусора. Требования умеренные: HTTPS или localhost, отдельный файл сценария и одна строка регистрации на странице. Safari подтянулся в 2018 году с версией 11.1, и с тех пор связка накрывает практически весь парк устройств, от старых корпоративных ноутбуков до дешёвых телефонов. Пример скелета со стартовым набором, уборкой и перехватом запросов:
const CACHE = "shell-v3";
const ASSETS = ["/", "/app.js", "/styles.css", "/offline.html"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
)
)
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((hit) => hit || fetch(event.request))
);
});
Жизненный цикл воркера от установки до обновления и ловушка ждущих вкладок
Установка это событие install: воркер складывает в хранилище оболочку приложения и обязан доложить об успехе, иначе браузер его отбракует, и старый продолжит работать. Активация это activate: сюда выносят уборку старых хранилищ и вызов clients.claim, который передаёт управление новому воркеру уже открытым страницам. Обновление устроено деликатнее, чем кажется: браузер сверяет файл воркера мимо обычного HTTP кэша, при расхождении ставит новенького на установку, но старый продолжает обслуживать открытые вкладки, пока те не закроются.
Вызовы skipWaiting и claim умеют форсировать момент, и здесь притаился классический подвох: страница в это мгновение держит в DOM старую разметку, и мгновенная перезагрузка без спроса смешивает версии на экране. Рабочее решение с уважением к пользователю: баннер о готовом обновлении и перезагрузка по клику. Регистрация со слушателем на свежую версию занимает несколько строк:
if ("serviceWorker" in navigator) {
const reg = await navigator.serviceWorker.register("/sw.js");
reg.addEventListener("updatefound", () => {
const worker = reg.installing;
worker.addEventListener("statechange", () => {
if (worker.state === "installed" && navigator.serviceWorker.controller) {
showUpdateBanner();
}
});
});
}
В разработке у воркеров свои приметы, без которых отладка превращается в работу вслепую. DevTools предлагает галочки Update on reload и Bypass for network: первая переворачивает жизненный цикл на каждой перезагрузке, вторая отдаёт все запросы в сеть мимо хранилища. Незарегистрированный воркер в тестовом контуре способен несколько дней тихо рулить трафиком, поэтому первым делом в консоли смотрят именно его.
Стратегии кэширования от cache first до stale while revalidate по типам ресурсов
Cache first это сначала хранилище, сеть только при промахе: годится для оболочки, шрифтов, иконок, всего, что меняется со сменой версии. Network first это сеть, хранилище в роли запасного аэродрома: путь для API и всего, что обязано быть свежим. Stale while revalidate отдаёт из хранилища немедленно и в фоне обновляет копию: идеал для лент, аватарок и всего живого, где секунды решают, а сутки нет.
Тонкость, о которой забывают: cache first привязывает файл к версии хранилища до следующего релиза, потому хэши в именах файлов и смена имени кэша при каждой сборке обязательны, иначе часть пользователей останется на старой версии навсегда. Правило большого пальца звучит так: precache для оболочки, runtime стратегии для остального, и ни одно правило не вешается вслепую на все запросы подряд. Запросы к API и запросы навигации живут по разным законам, и честный fetch обработчик начинается с их различения.
Для навигационных запросов есть отдельная техника: офлайн страница отдаётся не как замена всему сайту, а как честный ответ на невозможность загрузить документ, с сохранением исходного URL в строке браузера. Navigation preload разгружает медленные сети: браузер грузит документ параллельно запуску воркера, и сэкономленные сотни миллисекунд на дешёвом телефоне заметны без всяких метрик.
Миграция legacy PWA с AppCache на воркеры без потери работоспособности
Старый проект с манифестом переезжает спокойно и предсказуемо. Сначала аудит: собрать реальные запросы страницы в DevTools, сверить со списком манифеста, выкинуть лишние файлы и добавить забытые. Затем сборка precache списка, имя хранилища с версией, офлайн страница как подмена для навигации. Обязателен feature detection через проверку serviceWorker в navigator: на браузерах без воркера проект продолжает жить как обычный сайт, просто без оффлайн, и это честная цена широты поддержки. Старые секции манифеста переводятся по смыслу: CACHE превращается в precache список, NETWORK в правило живого трафика для API, FALLBACK в подмену навигации. Двухдневный аудит с протоколом запросов экономит недели догадок, потому что легаси списки почти всегда лгут и о составе, и о ролях файлов. Порядок шагов такой:
- собрать реальные запросы страницы в DevTools и сверить со списком манифеста;
- собрать precache список и дать хранилищу имя с версией;
- написать fetch обработчик со стратегиями по типам ресурсов;
- добавить офлайн страницу как подмену для навигационных запросов;
- повесить баннер обновления и перезагрузку по подтверждению;
- убрать атрибут манифеста из разметки после выката и удержания версии;
- следить за квотой через storage estimate и чистить старые хранилища.
После выката остаётся гигиена: атрибут манифеста убирают из тега html, файл манифеста снимают с сервера, а в отчёты добавляют метрику доли сессий с активным воркером, чтобы миграцию было видно в числах, а не в ощущениях.
И если старый манифест до сих пор раздаётся с сервера, лучший день начать переезд был вчера, второй лучший сегодня.
Workbox как готовый слой поверх Cache API для типовых оффлайн задач
Рукописный воркер хорош для понимания механики, но типовые задачи давно упакованы. Workbox даёт precacheAndRoute для стартового набора, registerRoute для правил по типам запросов и стратегии с говорящими именами: cacheFirst, networkFirst, staleWhileRevalidate. Сборочные плагины для webpack и Vite генерируют манифест precache на этапе сборки, сами подставляют хэши в имена и пересобирают список без участия рук.
Модуль expiration чистит хранилище по возрасту и числу записей, maxAgeSeconds и maxEntries на выбор. Плагин RangeRequests разруливает частичные запросы для видео и аудио, без которых медиа в оффлайн живёт плохо, а BroadcastUpdate оповещает страницу об обновлении кэша, и интерфейс перерисовывает пометку свежести без перезагрузки.
Писать руками стоит там, где логика нестандартная: уникальные условия подмены, эксперименты с версионированием, экзотические типы запросов. В остальном готовый слой экономит дни и оберегает от классических ошибок вроде забытой уборки старых хранилищ или вслепую повешенной стратегии на API. Миграция с AppCache на Workbox это ещё и переезд от ручного списка к генерируемому, и именно на этом шаге уходит главная боль старых манифестов: ручное ведение списка файлов.
Квота хранилища и уборка старых кэшей чтобы оффлайн не съел диск пользователя
У хранилища есть предел, и назывной кэш не резиновый. Метод navigator.storage.estimate возвращает пару usage и quota, и по ней видно, сколько оффлайн уже съел и сколько осталось. При нехватке места браузер по собственным правилам вычищает хранилища, и заявка на постоянство через navigator.storage.persist защищает важное приложение от тихой уборки. Уборка в коде держит всё предсказуемым: в activate остаются хранилища текущей версии, остальные уходят в корзину.
const { usage, quota } = await navigator.storage.estimate();
console.log(Math.round((usage / quota) * 100));
В телеметрию отправляют не только проценты заполнения, но и ошибки установки и активации: они честно рассказывают, где именно спотыкается релиз, и превращают техотчёт в короткий список мест для починки.
AppCache оставил индустрии урок, который стоит выучить наизусть: декларативность без контроля обернулась ловушкой, и тихий кэш стоил доверия больше, чем отсутствие кэша. Service Worker вернул контроль автору кода, Workbox упаковал рутину, а миграция старых проектов давно стала понятной процедурой на неделю. Оффлайн перестал быть мечтой веба и стал строчкой в конфиге сборки, и это достойный финал истории, которая началась с текстового манифеста.