Lazy loading стратегии

Библиотека Lottie Web использует JSON-описания анимаций, которые могут быть относительно тяжёлыми по объёму и затратными по рендерингу. При большом количестве анимаций на странице или наличии сложных сцен критичным становится контроль момента загрузки JavaScript-кода, JSON-файлов и инициализации canvas/SVG-рендера.

Lazy loading в контексте Lottie включает несколько уровней:

  • отложенная загрузка самой библиотеки
  • отложенная загрузка JSON-анимаций
  • отложенный запуск рендера
  • деинициализация и повторная инициализация при выходе из области видимости

Каждый уровень снижает нагрузку на основной поток и уменьшает время первого отображения страницы.


Динамический импорт библиотеки Lottie

Базовый сценарий — исключение Lottie из основного бандла и загрузка только при необходимости.

let lottieInstance = null;

async function loadLottie() {
  if (lottieInstance) return lottieInstance;

  const module = await import('lottie-web');
  lottieInstance = module.default;
  return lottieInstance;
}

Динамический import() позволяет:

  • уменьшить initial bundle size
  • отложить выполнение парсинга и выполнения кода библиотеки
  • загружать Lottie только в интерактивных частях интерфейса

В системах сборки (Vite, Webpack, Rollup) это приводит к созданию отдельного чанка, который подгружается по требованию.


Отложенная загрузка JSON-анимаций

JSON-файлы Lottie часто являются основным источником нагрузки. Их размер может варьироваться от десятков килобайт до нескольких мегабайт.

Стратегия — загрузка через fetch только при необходимости:

async function loadAnimationData(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error('Animation load error');
  return await response.json();
}

Оптимизация достигается за счёт:

  • загрузки только видимых анимаций
  • разделения ассетов по страницам или компонентам
  • использования CDN с кэшированием

IntersectionObserver для контроля видимости

Основной механизм ленивого запуска анимаций — наблюдение за попаданием элемента в viewport.

const observer = new IntersectionObserver(async (entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;

    const container = entry.target;
    observer.unobserve(container);

    const lottie = await loadLottie();
    const data = await loadAnimationData(container.dataset.src);

    lottie.loadAnimation({
      container,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      animationData: data
    });
  }
}, {
  threshold: 0.25
});

Ключевые параметры:

  • threshold регулирует момент старта загрузки
  • unobserve предотвращает повторную инициализацию
  • привязка data-src позволяет хранить путь к JSON прямо в DOM

Предзагрузка при приближении к области видимости

Lazy loading не всегда означает загрузку строго в момент появления. Более эффективный вариант — ранняя загрузка до попадания в viewport.

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) return;

    const rootMarginLoad = entry.rootBounds?.height || 0;

    if (entry.boundingClientRect.top < rootMarginLoad * 1.5) {
      preloadAnimation(entry.target.dataset.src);
    }
  });
}, {
  rootMargin: '200px'
});

Использование rootMargin позволяет инициировать загрузку заранее, уменьшая задержку перед стартом анимации.


Разделение загрузки: код, данные, рендер

Оптимальная модель Lottie lazy loading делит процесс на три независимых слоя:

  1. Загрузка библиотеки
  2. Загрузка JSON
  3. Создание animation instance

Каждый слой может быть кеширован и переиспользован.

const animationCache = new Map();

async function getAnimationData(src) {
  if (animationCache.has(src)) {
    return animationCache.get(src);
  }

  const data = await loadAnimationData(src);
  animationCache.set(src, data);

  return data;
}

Контроль повторной инициализации

При SPA-навигации или виртуальном скролле часто возникает повторное создание анимаций. Это приводит к утечкам памяти и перегрузке CPU.

Корректное управление жизненным циклом:

const instances = new WeakMap();

function createAnimation(container, data, lottie) {
  const instance = lottie.loadAnimation({
    container,
    renderer: 'svg',
    loop: true,
    autoplay: true,
    animationData: data
  });

  instances.set(container, instance);
  return instance;
}

function destroyAnimation(container) {
  const instance = instances.get(container);
  if (instance) {
    instance.destroy();
    instances.delete(container);
  }
}

Lazy hydration в списках анимаций

При наличии списков карточек с Lottie-анимациями используется стратегия частичной гидратации.

const items = document.querySelectorAll('.lottie-item');

items.forEach(item => observer.observe(item));

Дополнительная оптимизация:

  • инициализация только первых N элементов
  • последующая загрузка при скролле
  • приоритизация элементов в видимой зоне

Приоритизация загрузки анимаций

В условиях ограниченной пропускной способности полезно вводить очередь загрузки.

class LoadQueue {
  constructor(limit = 3) {
    this.limit = limit;
    this.active = 0;
    this.queue = [];
  }

  async add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.next();
    });
  }

  async next() {
    if (this.active >= this.limit || !this.queue.length) return;

    const { task, resolve, reject } = this.queue.shift();
    this.active++;

    try {
      const result = await task();
      resolve(result);
    } catch (e) {
      reject(e);
    } finally {
      this.active--;
      this.next();
    }
  }
}

Очередь ограничивает количество параллельных загрузок JSON и снижает пик нагрузки.


Условный рендер: только при необходимости

Некоторые анимации не требуют автоматического старта. Используется отложенный autoplay.

lottie.loadAnimation({
  container,
  renderer: 'svg',
  loop: false,
  autoplay: false,
  animationData
});

Запуск контролируется отдельно:

instance.play();

Это позволяет синхронизировать анимации с пользовательскими действиями или событиями интерфейса.


Оптимизация SVG-рендера при ленивой загрузке

SVG-режим в Lottie создаёт множество DOM-элементов. При большом количестве одновременно загруженных анимаций требуется контроль:

  • ограничение количества активных SVG-анимаций
  • переключение на canvas renderer при массовом рендере
  • уничтожение невидимых инстансов
lottie.loadAnimation({
  container,
  renderer: 'canvas',
  animationData
});

Canvas снижает нагрузку на DOM при высокой плотности анимаций.


Сброс и восстановление состояния при повторной видимости

При повторном входе элемента в viewport можно использовать восстановление прогресса:

const lastFrame = new Map();

function pauseAnimation(instance, id) {
  lastFrame.set(id, instance.currentFrame);
  instance.pause();
}

function resumeAnimation(instance, id) {
  const frame = lastFrame.get(id) || 0;
  instance.goToAndPlay(frame, true);
}

Это снижает необходимость полной перезагрузки JSON и пересоздания DOM.


Интеграция с виртуальным скроллом

В системах с виртуализированными списками lazy loading синхронизируется с жизненным циклом элементов:

  • mount → загрузка JSON
  • enter viewport → создание Lottie instance
  • unmount → destroy instance

Такой подход предотвращает накопление неиспользуемых анимаций при длинных списках.


Кэширование через HTTP и Service Worker

Для повторных посещений критично использование кэша:

  • CDN-кэш JSON
  • browser cache-control headers
  • service worker precache
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('.json')) {
    event.respondWith(
      caches.match(event.request).then(cached => cached || fetch(event.request))
    );
  }
});

Разделение критического и некритического контента

Анимации, влияющие на UX (индикаторы загрузки, интерактивные элементы), загружаются с более высоким приоритетом, чем декоративные.

Стратегия:

  • critical Lottie → preload
  • decorative Lottie → lazy
  • background Lottie → idle-time loading через requestIdleCallback
requestIdleCallback(() => {
  loadAnimationData(src);
});

Управление памятью при массовых анимациях

При большом количестве Lottie-инстансов ключевым становится контроль памяти:

  • обязательный destroy()
  • удаление ссылок из Map/WeakMap
  • остановка requestAnimationFrame внутри Lottie
instance.destroy();
instance = null;

Многоуровневый lazy loading pipeline

Полная схема выглядит как последовательность:

  1. IntersectionObserver фиксирует вход в область
  2. requestIdleCallback или очередь определяет приоритет
  3. fetch загружает JSON
  4. dynamic import подгружает Lottie
  5. создаётся instance
  6. autoplay запускается при необходимости

Каждый этап может быть независимым и кэшируемым, что позволяет масштабировать количество анимаций без деградации производительности.