Минимизация пересчетов

Universal Router — это гибкая библиотека маршрутизации для JavaScript, которая позволяет организовывать навигацию в приложениях без привязки к конкретному фреймворку. Основная единица маршрутизации — маршрут, который задаётся объектом с ключами path, action и опционально children.

Пример базового маршрута:

import UniversalRouter from 'universal-router';

const routes = [
  {
    path: '/',
    action: () => 'Главная страница',
  },
  {
    path: '/about',
    action: () => 'О нас',
  },
];

const router = new UniversalRouter(routes);

router.resolve('/about').then(result => {
  console.log(result); // "О нас"
});

Важным моментом является ленивая обработка маршрутов: action вызывается только тогда, когда маршрут совпадает, что уже снижает лишние вычисления.


Минимизация пересчетов при навигации

Пересчёты или повторные вызовы обработчиков маршрутов могут значительно замедлять работу приложения, особенно если маршруты содержат тяжёлые операции (загрузка данных, вычисления, рендер компонентов). Основные подходы к оптимизации:

1. Кэширование результатов маршрутов

Если результат маршрута зависит только от URL и не меняется при повторных переходах, имеет смысл использовать кэширование:

const cache = new Map();

const routes = [
  {
    path: '/users/:id',
    async action({ params }) {
      const key = params.id;
      if (cache.has(key)) return cache.get(key);
      const data = await fetch(`/api/users/${key}`).then(res => res.json());
      cache.set(key, data);
      return data;
    },
  },
];

Преимущество: при повторном переходе к /users/1 не выполняется запрос к серверу.

Важно: кэш нужно инвалидировать, если данные могут устареть.


2. Использование дочерних маршрутов (children) для локализации пересчетов

Universal Router поддерживает вложенные маршруты. Это позволяет разделить обработку и минимизировать пересчёты для неизменяемых сегментов:

const routes = [
  {
    path: '/dashboard',
    children: [
      {
        path: '/analytics',
        action: () => 'Аналитика',
      },
      {
        path: '/settings',
        action: () => 'Настройки',
      },
    ],
  },
];

При переходе внутри /dashboard пересчитываются только дочерние маршруты, а родительский блок остаётся неизменным.

Ключевой момент: маршруты строятся и проверяются сверху вниз, поэтому вложенная структура сокращает повторные вычисления для неизменяемых участков интерфейса.


3. Предварительная компиляция маршрутов

Universal Router позволяет строить маршруты динамически, но если структура известна заранее, стоит компилировать маршруты один раз:

const router = new UniversalRouter(routes, { resolveRoute: async (route, context) => {
  // кастомная обработка
}});

Эффект: не создаётся новая логика при каждом вызове resolve, уменьшая пересчёты путей и параметров.


4. Использование асинхронных действий для ленивых загрузок

Если часть маршрута требует тяжёлых операций (загрузка компонентов или данных), лучше вызывать их только при необходимости:

{
  path: '/reports',
  async action() {
    const { ReportsPage } = await import('./ReportsPage.js');
    return ReportsPage();
  },
}

Результат: модуль загружается только при переходе на /reports, что снижает нагрузку на начальную загрузку и пересчёты других маршрутов.


5. Мемоизация функций action

Если action использует повторяющиеся вычисления внутри маршрута, их стоит мемоизировать:

import memoizee from 'memoizee';

const computeHeavy = memoizee(async (param) => {
  // тяжёлое вычисление
  return await fetch(`/api/data/${param}`).then(res => res.json());
});

const routes = [
  {
    path: '/data/:id',
    async action({ params }) {
      return computeHeavy(params.id);
    },
  },
];

Вывод: при повторном вызове с тем же id операция выполняется один раз, что минимизирует пересчёты.


6. Отложенная проверка параметров

Universal Router передаёт параметры в action только после совпадения пути. Чтобы уменьшить лишние вычисления при сложных регулярных путях, можно использовать ленивое создание параметров:

{
  path: '/products/:category/:id',
  action: ({ params }) => {
    const id = parseInt(params.id, 10); // вычисление только при необходимости
    return `Категория ${params.category}, продукт ${id}`;
  },
}

Преимущество: вычисления выполняются только при точном совпадении пути.


7. Оптимизация глобального разрешения маршрутов

Universal Router выполняет последовательное сравнение маршрутов. Для больших приложений стоит:

  • Сортировать маршруты по частоте использования (часто вызываемые выше).
  • Избегать сложных wildcard-шаблонов на верхнем уровне, чтобы не проверять их при каждом переходе.
  • Использовать вложенные маршруты для локализации проверок.
const routes = [
  { path: '/', action: () => 'Home' },  // частый маршрут наверху
  { path: '/admin/:page', action: () => 'Admin' }, // менее частый
];

8. Сведение повторных переходов к минимуму

Даже при оптимизированных маршрутах лишние вызовы resolve создают пересчёты. Практика:

  • Хранить текущий маршрут и сравнивать перед вызовом resolve.
  • Игнорировать повторные переходы на один и тот же URL.
let currentPath = null;

async function navigate(path) {
  if (path === currentPath) return;
  currentPath = path;
  const result = await router.resolve(path);
  render(result);
}

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

Ключевые принципы: кэширование, ленивость, мемоизация, структуризация маршрутов и контроль повторных вызовов.