Precache и его отличие от runtime-кэширования

Service Worker предоставляет два фундаментальных подхода к работе с кэшем:

  • Precache (предварительное кэширование) — ресурсы загружаются и сохраняются в кэш заранее, на этапе установки Service Worker.
  • Runtime-кэширование (динамическое кэширование) — ресурсы добавляются в кэш по мере их запроса во время работы приложения.

Библиотека sw-precache реализует первый подход, автоматизируя процесс создания списка ресурсов и генерации Service Worker.


Суть precache-подхода

Precache — это стратегия, при которой набор файлов определяется заранее и гарантированно оказывается в кэше до начала работы приложения.

Ключевые характеристики:

  • список ресурсов известен на этапе сборки;
  • кэширование происходит в событии install Service Worker;
  • приложение может работать офлайн сразу после первой загрузки;
  • обновление кэша контролируется через версионирование файлов.

Как работает sw-precache

sw-precache — это инструмент сборки, который:

  1. Анализирует файловую систему проекта.
  2. Формирует список ресурсов (HTML, CSS, JS, изображения).
  3. Генерирует Service Worker с логикой precache.

Пример конфигурации:

const swPrecache = require('sw-precache');

swPrecache.write('./service-worker.js', {
  staticFileGlobs: [
    'public/**/*.html',
    'public/**/*.css',
    'public/**/*.js',
    'public/images/**/*.*'
  ],
  stripPrefix: 'public/'
});

Механизм установки и кэширования

Во время установки Service Worker выполняется:

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('precache-v1').then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/main.css',
        '/app.js'
      ]);
    })
  );
});

Особенности:

  • cache.addAll() гарантирует атомарность: либо кэшируются все ресурсы, либо ни один.
  • ошибка загрузки любого файла приводит к провалу установки Service Worker.

Версионирование и инвалидация кэша

Precache требует строгого контроля версий:

  • изменение содержимого файла должно менять его URL;
  • чаще всего используется хэш в имени файла:
app.3f5a8c.js
style.a91d2e.css

sw-precache автоматически:

  • добавляет ревизии файлов;
  • отслеживает изменения;
  • обновляет кэш при следующей установке Service Worker.

Обработка запросов при precache

После установки Service Worker перехватывает сетевые запросы:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

Поведение:

  • если ресурс есть в precache — он возвращается мгновенно;
  • если нет — запрос уходит в сеть.

Runtime-кэширование: противоположный подход

Runtime-кэширование работает иначе:

  • ресурсы не известны заранее;
  • добавляются в кэш по мере использования;
  • применяется к API-запросам, динамическому контенту.

Пример:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('runtime').then(cache => {
      return fetch(event.request).then(response => {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});

Ключевые различия precache и runtime-кэширования

1. Время кэширования

Подход Когда происходит
Precache При установке SW
Runtime Во время выполнения

2. Контроль над ресурсами

Подход Контроль
Precache Полный, список фиксирован
Runtime Частичный, зависит от запросов

3. Надёжность офлайн-режима

Подход Офлайн-доступ
Precache Гарантирован
Runtime Частичный

4. Производительность

  • Precache:

    • быстрый старт приложения;
    • отсутствие сетевых задержек для закэшированных файлов.
  • Runtime:

    • первая загрузка медленнее;
    • последующие — быстрее за счёт кэша.

5. Объём кэша

  • Precache:

    • фиксированный набор;
    • легко контролируется.
  • Runtime:

    • может расти бесконтрольно;
    • требует стратегий очистки.

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

Runtime-кэширование обычно использует стратегии:

  • Cache First
  • Network First
  • Stale While Revalidate

Пример стратегии Cache First:

event.respondWith(
  caches.match(event.request).then(cached => {
    return cached || fetch(event.request);
  })
);

Когда использовать precache

Подходит для:

  • статических ресурсов приложения;
  • SPA (Single Page Applications);
  • интерфейсных файлов (HTML, CSS, JS);
  • шрифтов и изображений.

Когда использовать runtime-кэширование

Используется для:

  • API-запросов;
  • пользовательских данных;
  • часто изменяемого контента;
  • больших файлов (например, видео).

Комбинированный подход

На практике применяется гибрид:

  • precache — для ядра приложения;
  • runtime — для динамических данных.

Пример архитектуры:

// precache (генерируется sw-precache)

// runtime
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(networkFirstStrategy(event.request));
  }
});

Ограничения precache

  • необходимость пересборки проекта при изменениях;
  • увеличение времени установки Service Worker;
  • неэффективен для динамических данных;
  • требует контроля версий.

Особенности sw-precache

  • автоматически генерирует список ресурсов;
  • внедряет хэширование;
  • минимизирует ручную работу;
  • поддерживает fallback-страницы;
  • может интегрироваться с сборщиками (Webpack, Gulp).

Ошибки при использовании precache

1. Отсутствие версионирования

  • приводит к устаревшим данным в кэше.

2. Слишком большой список файлов

  • увеличивает время установки.

3. Кэширование API-ответов

  • нарушает логику обновления данных.

4. Игнорирование обновления Service Worker

  • пользователи остаются на старой версии.

Обновление precache

Процесс:

  1. Изменяется файл проекта.
  2. Генерируется новый Service Worker.
  3. Браузер устанавливает его в фоне.
  4. Новый SW активируется после закрытия старых вкладок.

Контроль обновления:

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.map(key => {
          if (key !== 'precache-v2') {
            return caches.delete(key);
          }
        })
      );
    })
  );
});

Итоговое различие на уровне архитектуры

  • Precache — декларативный подход: ресурсы известны заранее.
  • Runtime — реактивный подход: ресурсы кэшируются по факту использования.

Именно precache обеспечивает мгновенную загрузку и офлайн-работу, тогда как runtime-кэширование отвечает за гибкость и актуальность данных в процессе эксплуатации приложения.