Паттерн ленивой загрузки

Паттерн ленивой загрузки откладывает инициализацию timeago.js до момента, когда она действительно нужна. Это ускоряет начальную загрузку страницы и снижает нагрузку на CPU.


Зачем нужна ленивая загрузка

Инициализация render для 200 элементов сразу создаёт 200 таймеров одновременно. Если большинство из них не видно пользователю (например, находятся ниже fold или во вкладках), это бессмысленная нагрузка.


Ленивый импорт библиотеки

// Основной бандл — timeago.js не включается
// Загружается только при первом использовании
async function initTimeago(elements) {
  const { render, cancel } = await import('timeago.js');
  const { default: ru }    = await import('timeago.js/esm/lang/ru');
  const { register }       = await import('timeago.js');

  register('ru', ru);
  render(elements, 'ru');

  return () => cancel(elements);
}

Ленивая инициализация через IntersectionObserver

const initialized = new WeakSet();

const io = new IntersectionObserver(async (entries) => {
  const toInit = entries
    .filter(e => e.isIntersecting && !initialized.has(e.target))
    .map(e => e.target);

  if (toInit.length === 0) return;

  const { render } = await import('timeago.js');
  const { register } = await import('timeago.js');
  const { default: ru } = await import('timeago.js/esm/lang/ru');

  register('ru', ru);

  toInit.forEach(el => {
    initialized.add(el);
    render(el, 'ru');
  });
}, { threshold: 0.1 });

document.querySelectorAll('[datetime]').forEach(el => io.observe(el));

Ленивая загрузка локалей

Загружать локаль только при первом использовании:

const loadedLocales = new Set();

async function formatLazy(date, locale = 'ru') {
  if (!loadedLocales.has(locale)) {
    const { register } = await import('timeago.js');
    const mod = await import(`timeago.js/esm/lang/${locale}.js`);
    register(locale, mod.default);
    loadedLocales.add(locale);
  }

  const { format } = await import('timeago.js');
  return format(date, locale);
}

React: ленивая инициализация хука

import { useState, useEffect } from 'react';

function useLazyTimeAgo(date: Date | string, locale = 'ru'): string {
  const [text, setText] = useState('');

  useEffect(() => {
    let cleanup: (() => void) | undefined;

    import('timeago.js').then(({ format, render, cancel, register }) => {
      import(`timeago.js/esm/lang/${locale}.js`).then(mod => {
        register(locale, mod.default);
        setText(format(date, locale));

        // Автообновление после загрузки
        const id = setInterval(() => {
          setText(format(date, locale));
        }, 60_000);

        cleanup = () => clearInterval(id);
      });
    });

    return () => cleanup?.();
  }, [date, locale]);

  return text;
}

Vue: компонент с ленивой загрузкой

<template>
  <time :datetime="isoDate" ref="timeEl">{{ fallbackText }}</time>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';

const props = defineProps<{ date: string; locale?: string }>();
const timeEl = ref<HTMLTimeElement | null>(null);
let cleanup: (() => void) | null = null;

const isoDate = computed(() => new Date(props.date).toISOString());
const fallbackText = computed(() => new Date(props.date).toLocaleDateString('ru-RU'));

onMounted(async () => {
  const [{ render, cancel, register }, mod] = await Promise.all([
    import('timeago.js'),
    import(`timeago.js/esm/lang/${props.locale ?? 'ru'}.js`),
  ]);

  register(props.locale ?? 'ru', mod.default);

  if (timeEl.value) {
    render(timeEl.value, props.locale ?? 'ru');
    cleanup = () => { if (timeEl.value) cancel(timeEl.value); };
  }
});

onUnmounted(() => cleanup?.());
</script>

Ленивая загрузка при прокрутке (виртуальный список)

// При использовании виртуальных списков (react-window, vue-virtual-scroller)
// элементы монтируются и размонтируются по мере прокрутки

// В обработчике монтирования виртуального элемента:
async function onItemMount(el) {
  const { render } = await import('timeago.js');
  render(el.querySelector('time'), 'ru');
}

async function onItemUnmount(el) {
  const { cancel } = await import('timeago.js');
  cancel(el.querySelector('time'));
}

Одноразовая инициализация с Promise

// Инициализировать timeago.js один раз, переиспользовать
const timeagoPromise = import('timeago.js').then(async (lib) => {
  const [ruMod] = await Promise.all([
    import('timeago.js/esm/lang/ru'),
  ]);
  lib.register('ru', ruMod.default);
  return lib;
});

// Использование
async function formatDate(date) {
  const { format } = await timeagoPromise;
  return format(date, 'ru');
}

Преимущества ленивой загрузки

Метрика Без ленивой загрузки С ленивой загрузкой
Начальный бандл +2-3 KB timeago.js 0 KB при загрузке страницы
Время до интерактивности Медленнее Быстрее
Активные таймеры Все сразу Только для видимых элементов
Нагрузка на CPU Высокая при старте Распределена во времени