Code splitting по маршрутам

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

В Universal Router маршруты описываются как массив объектов, каждый из которых содержит путь и обработчик:

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/home',
    action: () => import('./pages/Home.js').then(module => module.default)
  },
  {
    path: '/about',
    action: () => import('./pages/About.js').then(module => module.default)
  }
];

const router = new UniversalRouter(routes);

В примере выше используется динамический import(), который возвращает Promise. Это позволяет отложенно загружать код страницы только при переходе на соответствующий маршрут.


Структура маршрутов для Code Splitting

При использовании code splitting рекомендуется придерживаться следующих принципов:

  1. Изоляция страниц в отдельные модули Каждая страница или компонент маршрута должна находиться в отдельном файле. Это упрощает ленивую загрузку:
// pages/Home.js
export default function Home() {
  return `<h1>Главная страница</h1>`;
}
  1. Ленивые action-функции Обработчики маршрутов должны возвращать промис, который разрешается динамически импортированным модулем:
{
  path: '/contact',
  action: () => import('./pages/Contact.js').then(module => module.default)
}
  1. Поддержка вложенных маршрутов Code splitting можно применять и к вложенным маршрутам, что позволяет загружать только нужные подмодули:
{
  path: '/dashboard',
  children: [
    {
      path: '/stats',
      action: () => import('./pages/dashboard/Stats.js').then(m => m.default)
    },
    {
      path: '/settings',
      action: () => import('./pages/dashboard/Settings.js').then(m => m.default)
    }
  ]
}

Асинхронные action и обработка ошибок

Поскольку динамический импорт возвращает промис, важно правильно обрабатывать ошибки загрузки. Например:

{
  path: '/profile',
  action: async () => {
    try {
      const module = await import('./pages/Profile.js');
      return module.default;
    } catch (err) {
      console.error('Ошибка загрузки страницы Profile', err);
      return '<h1>Не удалось загрузить страницу</h1>';
    }
  }
}

Это предотвращает падение всего приложения, если модуль по какой-то причине не загрузился.


Code Splitting и серверный рендеринг

Universal Router поддерживает серверный рендеринг, и для него необходимо учитывать, что import() работает асинхронно. В Node.js это тоже промис, и нужно корректно обрабатывать его внутри серверного маршрутизатора:

import express from 'express';
import UniversalRouter from 'universal-router';

const app = express();

app.get('*', async (req, res) => {
  try {
    const routeResult = await router.resolve({ pathname: req.path });
    res.send(routeResult);
  } catch (err) {
    res.status(500).send('Ошибка сервера');
  }
});

Такой подход обеспечивает одинаковое использование ленивых модулей на клиенте и сервере, сохраняя преимущества code splitting.


Интеграция с Webpack и другими сборщиками

Для эффективного code splitting важно правильно настроить сборщик. В Webpack динамический импорт автоматически создает отдельные чанки:

{
  path: '/blog',
  action: () => import(/* webpackChunkName: "blog" */ './pages/Blog.js').then(m => m.default)
}
  • webpackChunkName позволяет давать понятные имена создаваемым бандлам.
  • Можно использовать /* webpackPrefetch: true */ для предзагрузки модулей, если известно, что пользователь может перейти на страницу вскоре.

Пример с предзагрузкой:

{
  path: '/shop',
  action: () => import(/* webpackChunkName: "shop", webpackPrefetch: true */ './pages/Shop.js')
           .then(m => m.default)
}

Рекомендации по организации Code Splitting

  • Минимизировать зависимости между модулями — чем меньше общий код, тем эффективнее ленивые загрузки.
  • Использовать вложенные роуты для крупных разделов — подгружать только нужные компоненты.
  • Добавлять fallback UI для отображения загрузки контента:
{
  path: '/lazy',
  action: async () => {
    const Loading = '<div>Загрузка...</div>';
    const promise = import('./pages/LazyPage.js').then(m => m.default);
    return promise.catch(() => '<h1>Ошибка загрузки</h1>') || Loading;
  }
}
  • Разделять большие страницы на подкомпоненты для дополнительной оптимизации.

Пример полного маршрута с code splitting и вложенными роутами

const routes = [
  {
    path: '/',
    action: () => import('./pages/Home.js').then(m => m.default)
  },
  {
    path: '/dashboard',
    children: [
      {
        path: '/stats',
        action: () => import('./pages/dashboard/Stats.js').then(m => m.default)
      },
      {
        path: '/settings',
        action: () => import('./pages/dashboard/Settings.js').then(m => m.default)
      }
    ]
  },
  {
    path: '/blog/:id',
    action: ({ params }) => import('./pages/BlogPost.js')
                      .then(m => m.default(params))
  }
];

const router = new UniversalRouter(routes);

В этом примере:

  • Главная страница и блог подгружаются только по необходимости.
  • Внутри dashboard загружаются отдельные модули для статистики и настроек.
  • Маршрут /blog/:id динамически передает параметры в ленивый модуль.

Эта организация маршрутов с динамическими import() позволяет достичь максимальной оптимизации загрузки и отзывчивости приложения, полностью используя возможности Universal Router для Code Splitting.