Мемоизация результатов

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

Основы работы с resolve

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

const route = {
  path: '/user/:id',
  action: async ({ params }) => {
    return fetchUser(params.id);
  }
};

Если пользователь несколько раз открывает один и тот же путь /user/123, то без мемоизации каждый вызов action инициирует новый HTTP-запрос. Это неэффективно.

Простейшая мемоизация

Мемоизация функции action может быть реализована с помощью замыкания:

function memoizeAction(fn) {
  const cache = new Map();
  return async (context) => {
    const key = JSON.stringify(context.params);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = await fn(context);
    cache.set(key, result);
    return result;
  };
}

const route = {
  path: '/user/:id',
  action: memoizeAction(async ({ params }) => {
    return fetchUser(params.id);
  })
};

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

  • Ключ кэширования формируется на основе params маршрута.
  • Кэш хранится в памяти, что позволяет повторно использовать результат без лишних запросов.
  • Асинхронные функции обрабатываются корректно через await.

Мемоизация для сложных маршрутов

Для маршрутов с вложенными children мемоизация может быть применена на нескольких уровнях:

const routes = [
  {
    path: '/dashboard',
    action: memoizeAction(async () => {
      return loadDashboardData();
    }),
    children: [
      {
        path: 'stats',
        action: memoizeAction(async () => {
          return loadStatsData();
        })
      },
      {
        path: 'reports',
        action: memoizeAction(async () => {
          return loadReportsData();
        })
      }
    ]
  }
];

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

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

Управление сроком жизни кэша

В некоторых случаях важно ограничить время жизни кэшированных данных:

function memoizeWithTTL(fn, ttl = 5000) {
  const cache = new Map();
  return async (context) => {
    const key = JSON.stringify(context.params);
    const now = Date.now();
    if (cache.has(key)) {
      const { value, expires } = cache.get(key);
      if (now < expires) return value;
    }
    const result = await fn(context);
    cache.set(key, { value: result, expires: now + ttl });
    return result;
  };
}

const route = {
  path: '/user/:id',
  action: memoizeWithTTL(async ({ params }) => {
    return fetchUser(params.id);
  }, 10000)
};

Преимущества такого подхода:

  • Обновление данных через заданный интервал времени.
  • Контроль памяти — устаревшие данные автоматически заменяются.
  • Повышение производительности при частых запросах к одним и тем же маршрутам.

Использование глобального кэша

Иногда выгодно централизовать кэш для всего роутера:

const globalCache = new Map();

const memoizeGlobal = (fn) => async (context) => {
  const key = context.pathname;
  if (globalCache.has(key)) return globalCache.get(key);
  const result = await fn(context);
  globalCache.set(key, result);
  return result;
};

const router = new UniversalRouter([
  { path: '/users', action: memoizeGlobal(loadUsers) },
  { path: '/settings', action: memoizeGlobal(loadSettings) }
]);

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

  • Ключ кэширования строится из полного pathname.
  • Удобно для страниц, которые не зависят от параметров, либо имеют уникальный URL.
  • Требует отдельного механизма очистки, если данные могут меняться.

Комбинирование с Lazy Loading

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

const route = {
  path: '/profile/:id',
  action: memoizeAction(async ({ params }) => {
    const module = await import('./Profile.js');
    return module.renderProfile(params.id);
  })
};
  • Асинхронный импорт выполняется один раз для каждого набора параметров.
  • Повторные переходы к тому же пользователю не вызывают повторного импорта или рендера.
  • Экономит время и ресурсы, особенно при больших SPA-приложениях.

Важные рекомендации

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

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