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

Ленивая загрузка в контексте библиотек для веб-компонентов — это стратегия оптимизации, позволяющая загружать и инициализировать компоненты только тогда, когда они действительно нужны. В Haunted, библиотеке для создания реактивных веб-компонентов на основе Hooks, ленивые компоненты реализуются через динамический импорт и использование асинхронных хуков. Это позволяет уменьшить первоначальный вес страницы и ускорить время первой отрисовки.


Динамический импорт компонентов

В Haunted компонент — это функция, которая возвращает шаблон с помощью html. Для ленивой загрузки можно использовать стандартный синтаксис динамического импорта:

import { component, html } from 'haunted';

const LazyComponent = async () => {
  const module = await import('./MyComponent.js');
  return module.MyComponent;
};

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

  • Файл MyComponent.js не включается в основной бандл, а загружается при необходимости.
  • LazyComponent сам по себе является функцией, возвращающей промис с компонентом.
  • Для корректного использования требуется обернуть вызов в асинхронный контекст или использовать вспомогательную обертку.

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

Haunted предоставляет стандартный набор хуков (useState, useEffect), с помощью которых можно управлять состоянием загрузки. Пример организации ленивого компонента с индикатором загрузки:

import { html, component, useState, useEffect } from 'haunted';

function AsyncWrapper({ loader }) {
  const [LoadedComponent, setLoadedComponent] = useState(null);

  useEffect(() => {
    let isMounted = true;
    loader().then((module) => {
      if (isMounted) {
        setLoadedComponent(() => module.default || module);
      }
    });
    return () => { isMounted = false; };
  }, [loader]);

  return LoadedComponent
    ? html`<${LoadedComponent}></${LoadedComponent}>`
    : html`<p>Загрузка...</p>`;
}

customElements.define('async-wrapper', component(AsyncWrapper));

Пояснение:

  • loader — функция, возвращающая промис динамического импорта.
  • useState хранит загруженный компонент после разрешения промиса.
  • useEffect гарантирует, что загрузка произойдет только при монтировании компонента, а также предотвращает утечки памяти при размонтировании.
  • Шаблон <p>Загрузка...</p> может быть заменен на любой индикатор прогресса.

Интеграция ленивой загрузки с существующими компонентами

Для компонентов, которые активно используются в интерфейсе, полезно комбинировать ленивую загрузку с условным рендерингом:

import { html, component, useState } from 'haunted';

function Dashboard() {
  const [showSettings, setShowSettings] = useState(false);

  return html`
    <button @click=${() => setShowSettings(true)}>Открыть настройки</button>
    ${showSettings
      ? html`<async-wrapper .loader=${() => import('./Settings.js')}></async-wrapper>`
      : ''}
  `;
}

customElements.define('dashboard-view', component(Dashboard));

Особенности такого подхода:

  • Настройки загружаются только при первом клике.
  • Основной бандл не содержит кода ненужного на старте, уменьшая время загрузки страницы.
  • async-wrapper обеспечивает корректный рендер во время асинхронной загрузки.

Управление ошибками при загрузке

Асинхронные компоненты могут завершаться с ошибкой, поэтому важно предусматривать обработку исключений:

useEffect(() => {
  let isMounted = true;
  loader()
    .then((module) => {
      if (isMounted) setLoadedComponent(() => module.default || module);
    })
    .catch((error) => {
      console.error('Ошибка при загрузке компонента:', error);
      if (isMounted) setLoadedComponent(() => () => html`<p>Не удалось загрузить компонент</p>`);
    });
  return () => { isMounted = false; };
}, [loader]);

Рекомендации:

  • Использовать отдельный компонент-заглушку для отображения ошибок.
  • Логировать ошибки в консоль или отправлять на сервер для анализа.
  • При необходимости добавлять повторную попытку загрузки.

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

  1. Кэширование компонентов: после первой загрузки компонент хранится в памяти, что позволяет избежать повторных динамических импортов.
  2. Предзагрузка: для компонентов, которые, вероятно, будут нужны в ближайшее время, можно использовать <link rel="preload"> или динамический импорт с таймаутом.
  3. Мелкие чанки: разбивать большие компоненты на более мелкие, чтобы уменьшить время загрузки одного файла.
  4. Комбинация с Suspense-подобной логикой: хотя Haunted не имеет встроенного Suspense, можно реализовать аналог, используя useState и условный рендеринг, как показано в примере с AsyncWrapper.

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

import { html, component, useState, useEffect } from 'haunted';

function LazyTabs() {
  const [activeTab, setActiveTab] = useState('home');
  const [TabComponent, setTabComponent] = useState(null);

  useEffect(() => {
    let isMounted = true;
    const loader = {
      home: () => import('./HomeTab.js'),
      profile: () => import('./ProfileTab.js'),
      settings: () => import('./SettingsTab.js'),
    }[activeTab];

    loader()
      .then((mod) => { if (isMounted) setTabComponent(() => mod.default); })
      .catch((err) => { console.error(err); });

    return () => { isMounted = false; };
  }, [activeTab]);

  return html`
    <nav>
      <button @click=${() => setActiveTab('home')}>Home</button>
      <button @click=${() => setActiveTab('profile')}>Profile</button>
      <button @click=${() => setActiveTab('settings')}>Settings</button>
    </nav>
    ${TabComponent ? html`<${TabComponent}></${TabComponent}>` : html`<p>Загрузка вкладки...</p>`}
  `;
}

customElements.define('lazy-tabs', component(LazyTabs));

Особенности реализации:

  • Разделение каждой вкладки на отдельный ленивый модуль.
  • Автоматическое подгружание нужной вкладки при смене состояния.
  • Использование состояния activeTab для управления текущим активным компонентом.

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