Code splitting

Code splitting — техника разделения кода на отдельные части (чанки), которые загружаются по мере необходимости. В контексте клиентских приложений на JavaScript это позволяет уменьшить начальный размер бандла и ускорить первичную загрузку страницы. В библиотеке Navigo code splitting реализуется через динамический импорт модулей в обработчиках маршрутов.


Динамические обработчики маршрутов

Navigo позволяет назначать функции-обработчики на каждый маршрут. Вместо того чтобы импортировать все модули сразу, можно использовать динамический import():

import Navigo from 'navigo';

const router = new Navigo('/');

router.on('/home', async () => {
    const { default: homeModule } = await import('./modules/home.js');
    homeModule.init();
});

router.on('/about', async () => {
    const { default: aboutModule } = await import('./modules/about.js');
    aboutModule.init();
});

router.resolve();

Ключевые моменты:

  • import('./modules/home.js') возвращает промис.
  • Объект модуля содержит свойство default — стандартный экспорт ES6.
  • Функция init() отвечает за рендеринг или логику страницы.

Таким образом, код модуля подгружается только тогда, когда пользователь переходит на соответствующий маршрут.


Lazy loading и асинхронные компоненты

В больших приложениях часто используется концепция lazy loading компонентов. Navigo позволяет подключать асинхронные компоненты аналогично:

router.on('/dashboard', async () => {
    const { Dashboard } = await import('./components/Dashboard.js');
    const dashboard = new Dashboard();
    dashboard.render();
});

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

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

Группировка и именованные чанки

Современные сборщики, такие как Webpack, поддерживают именование чанков. Это упрощает отладку и контроль загрузки:

router.on('/profile', async () => {
    const { default: profileModule } = await import(
        /* webpackChunkName: "profile" */ './modules/profile.js'
    );
    profileModule.init();
});

Преимущества именованных чанков:

  • Чанк получает понятное имя в сети (profile.js), вместо генерации случайного хеша.
  • Улучшается читаемость логов и профайлинг загрузки.
  • Можно легко настраивать приоритизацию загрузки для критических страниц.

Обработка ошибок при динамической загрузке

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

router.on('/settings', async () => {
    try {
        const { default: settingsModule } = await import('./modules/settings.js');
        settingsModule.init();
    } catch (err) {
        console.error('Ошибка загрузки модуля settings:', err);
        document.body.innerHTML = '<h1>Ошибка загрузки страницы</h1>';
    }
});

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

  • Всегда использовать блок try/catch для промисов динамических импортов.
  • Предусматривать fallback-контент или страницу ошибки для пользователя.
  • Логировать ошибки для дальнейшего анализа.

Параллельная загрузка нескольких модулей

Если страница зависит от нескольких модулей, их можно загружать параллельно с помощью Promise.all:

router.on('/analytics', async () => {
    try {
        const [chartsModule, tableModule] = await Promise.all([
            import('./modules/charts.js'),
            import('./modules/table.js')
        ]);
        chartsModule.default.init();
        tableModule.default.init();
    } catch (err) {
        console.error('Ошибка загрузки модулей analytics:', err);
    }
});

Преимущества:

  • Уменьшается общее время загрузки, так как запросы выполняются параллельно.
  • Можно комбинировать зависимости без увеличения времени отклика страницы.

Применение с состоянием приложения

При использовании code splitting важно учитывать состояние приложения. Например, если модуль зависит от глобального состояния или store:

import store from './store.js';

router.on('/cart', async () => {
    const { default: cartModule } = await import('./modules/cart.js');
    cartModule.init(store.getState());
});

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

  • Состояние передается в динамически загружаемый модуль.
  • Модули остаются независимыми, но могут получать данные из централизованного хранилища.
  • Можно комбинировать с реактивными библиотеками для автоматического обновления UI после загрузки.

Совместимость с переходами без перезагрузки

Navigo автоматически перехватывает переходы по ссылкам внутри приложения. Это позволяет применять code splitting без перезагрузки страницы:

document.querySelectorAll('a[data-navigo]').forEach(link => {
    link.addEventListener('click', e => {
        e.preventDefault();
        router.navigate(link.getAttribute('href'));
    });
});

Преимущества:

  • Динамический импорт срабатывает только при переходе по маршруту.
  • Страницы загружаются по мере необходимости, без блокировки основного потока.
  • Ускоряется взаимодействие пользователя с приложением.

Практические советы по оптимизации

  • Разбивать код по смысловым блокам, соответствующим маршрутам.
  • Использовать именованные чанки для крупных модулей.
  • Предусматривать fallback-контент или спиннеры во время загрузки.
  • Загружать критические зависимости сразу, а второстепенные — лениво.
  • Параллельно загружать независимые модули для ускорения рендеринга.

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