Intersection Observer + Service Worker + Cache API

Привет! У меня на сайте основная страница имеет много JS кода. Для оптимизации загрузки я сделал загрузку скриптов интерактивной географической карты через Intersection Observer:

const sectionMap = document.querySelector(".js-section-map");
const observerOptions = { rootMargin: '100px' };


if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/assets/js/map/sw.bundle.js')
    .then(reg => {
      console.log('Service Worker для карты зарегистрирован', reg);
    })
    .catch(err => {
      console.warn('Ошибка регистрации Service Worker для карты:', err);
    });
}

if (sectionMap) {
  const observer = new IntersectionObserver(onIntersection, observerOptions);
  observer.observe(sectionMap);

  function onIntersection(entries, observer) {
    entries.forEach(async (entry) => {
      if (entry.isIntersecting) {
        try {
          await import(/* webpackChunkName: "map" */ './map.mjs');
          await import(/* webpackChunkName: "modalMap" */ './modalMap.mjs');
        } catch (error) {
          console.error('Ошибка загрузки карты:', error);
        }
        observer.unobserve(entry.target);
      }
    })
  }
}



Логика такая: когда пользователь докрутит до блока с картой, срабатывает IO и загружает нужные скрипты - в коде динамический импорт. Однако ниже блока с картой находится много блочков с кнопкой вызова модалки, в которую вставляется эта карта. Кнопки активируются тоже после срабатывания IO (modalMap.mjs). Всё работает. Но! Если пользователь перезагружает страницу, он остаётся на том уровне страницы, где случилась перезагрузка. И если карту не пересекали, карта и кнопки вызова модалки не работают. Нужно снова крутить вверх до карты, чтобы IO опять загрузил скрипты, заработала карта и кнопки, а затем крутить вниз до места, где остановились. Ужасно неудобно.

Пришла идея использовать Service Worker (сайт - аудиогид, в основном для мобильных устройств, поэтому захотелось сделать работу карты также в офлайн режиме). Написал такой код:

const CACHE_NAME = 'map-scripts-cache-v1';
const MAP_SCRIPTS_PATH = '/assets/js/map/'; // путь от корня сайта

self.addEventListener('install', (event) => {
  // При установке SW сразу кэшируем все нужные файлы из папки map.
  // Чтобы минимизировать, список можно держать статичным или сгенерировать при билде.

  const urlsToCache = [
    `${MAP_SCRIPTS_PATH}mapLazy.bundle.js`,
    `${MAP_SCRIPTS_PATH}map.bundle.js`,
    `${MAP_SCRIPTS_PATH}modalMap.bundle.js`,
    `${MAP_SCRIPTS_PATH}leaflet.bundle.js`,
  ];

  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
      .then(() => self.skipWaiting())
  );
});

self.addEventListener('activate', (event) => {
  // Удаляем старые кеши при обновлении SW
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
      ))
      .then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', (event) => {
  // Перехватываем запросы к нашим скриптам из папки map
  if (event.request.url.includes(MAP_SCRIPTS_PATH)) {
    event.respondWith(
      caches.match(event.request)
        .then(cachedResponse => {
          if (cachedResponse) {
            return cachedResponse;
          }
          // Если в кеше нет — запросим из сети и добавим в кеш
          return fetch(event.request).then(networkResponse => {
            return caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, networkResponse.clone());
              return networkResponse;
            });
          });
        })
    );
  }
});

Всё работает, но! Тогда скрипты - в т.ч. которые были отложены для IO - грузятся в кэш сразу. Я не до конца понимаю ещё Service Worker и возникают вопросы:

  1. Раз скрипты выкачиваются сразу, то исчезает смысл в IO. Но, тогда, получается, убивается идея ускорить загрузку страницу при первичном заходе на неё!
  2. Попробовали с ИИ переписать код, чтобы SW перехватывал запрос в момент срабатывания IO, забирал скрипты и записывал их в кэш. Но он НЕ записывает ничего в кэш в такой ситуации. Получается, что fetch SW срабатывает только в самом начале загрузки страницы и никак с IO не общается?
  3. Я прочитал, что, вроде, SW это вообще отдельный поток и он non-blocking. Получается, тогда идея IO вообще лишена смысла?
  4. Если IO в таком случае не нужен, как переписать IO, чтобы он всегда грузил скрипты только из кэша?

Помогите, пожалуйста, кто разбирается в этой технологии. Ещё раз очерчу цель:

  1. Максимально ускорить загрузку странице при первичном заходе на сайт (карта не в первом экране) на мобильных
  2. Иметь сразу загруженные скрипты, если карту пересекали, но перезагрузили страницу - чтобы сразу были активны кнопки вызова модалки.
1 лайк

может при DOMContentLoaded с какой то задержкой проверить - а ести ли сейчас в зоне видимости карта и кнопки вызова модалки ? и если есть то подгрузить/активировать нужные скрипты
создать что вроде
function isElementInViewport(el: HTMLElement): boolean {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);

ну такое не уверен что это хорошая идея, но то что пришло в голову

1 лайк

В любом случае, я признателен за внимание к посту и любую идею ) Пробовали с задержкой - всё равно не работает. Как я понял, SW не работает отсроченно.

От офлайна пришлось отказаться, т.к. карта грузит тайлы с внешних источников и если нет интернета - нет карты вообще. Создавать сервер карты на хостинге - в любом случае, это опять хостинг и связь с ним через интернет, да и пока такое для меня тёмный лес. Может, когда-нибудь, когда дорасту, сделаю приложение, которое можно будет скачать на мобильный, тогда будет смысл вернуться к этой идее, но сомнительно, что дорасту до этого.

Остальное решили так: при первом посещении страницы, если до карты доскроллили, записываем в sessionStorage булев флаг. Если страницу перезагрузили с какого-то места, значит, флаг есть и тогда сразу грузим скрипты карты. После открытия новой вкладки браузер всё равно покажет страницу с начала, значит, флага нет и ждём “встречи” с картой. Всё работает корректно.

После принятия решения Google, наконец, устами своего ИИ после очередного запроса выдал мысль, что в данной ситуации нужно регистрировать и инициировать SW как раз внутри Intersection Observer, тогда он запустится только при пересечении карты, сразу выкачает и закэширует нужные скрипты. Но пока что это не проверяли. Как-нибудь на досуге.

Я пока не буду отмечать этот пост как “решённый” - может, сюда зайдёт кто-то, кто лучше знаком с SW, сталкивался с подобными ситуациями и сможет что-то прояснить.

1 лайк