Scroll restoration

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


Настройка scroll restoration

Universal Router не предоставляет встроенного API для управления скроллом. Вместо этого разработчики реализуют это через callback-функции в resolve или используя middleware-решения. Основная идея состоит в том, чтобы после вычисления маршрута программно изменить положение окна с помощью методов:

window.scrollTo(x, y);
window.scroll({ top: y, left: x, beh * avior: 'smooth' });

Пример интеграции scroll restoration в маршрутизатор:

import UniversalRouter from 'universal-router';

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

const router = new UniversalRouter(routes);

async function navigate(path) {
  const module = await router.resolve({ pathname: path });
  
  if (module && module.default) {
    document.getElementById('app').innerHTML = module.default();
  }

  restoreScroll(path);
}

const scrollPositions = new Map();

function restoreScroll(path) {
  const position = scrollPositions.get(path);
  if (position) {
    window.scrollTo(position.x, position.y);
  } else {
    window.scrollTo(0, 0);
  }
}

window.addEventListener('scroll', () => {
  const path = window.location.pathname;
  scrollPositions.set(path, { x: window.scrollX, y: window.scrollY });
});

Ключевые моменты в этом подходе:

  • Сохраняется положение скролла при уходе с текущего маршрута.
  • При возврате на маршрут оно восстанавливается.
  • Если позиция для маршрута не сохранена, скролл перемещается на верх страницы.
  • Поддерживается плавная анимация через beh * avior: 'smooth', что улучшает UX.

Scroll Restoration и асинхронные маршруты

При использовании динамического импорта компонентов (import()) важно учитывать, что страница может загружаться с задержкой. В таком случае попытка восстановить скролл сразу после resolve может сработать до полной отрисовки контента, что приведёт к неправильной позиции.

Решение — вызов восстановления скролла после рендера контента:

async function navigate(path) {
  const module = await router.resolve({ pathname: path });
  
  if (module && module.default) {
    document.getElementById('app').innerHTML = module.default();
    
    // Восстановление скролла после отрисовки
    requestAnimationFrame(() => {
      restoreScroll(path);
    });
  }
}

Использование requestAnimationFrame гарантирует, что браузер завершил перерисовку DOM перед установкой позиции скролла.


Продвинутые сценарии: сохранение скролла для отдельных элементов

Иногда требуется сохранять прокрутку не всего окна, а отдельного контейнера. В этом случае подход аналогичен, но используется element.scrollTop и element.scrollLeft:

const scrollContainer = document.getElementById('scrollable-container');

window.addEventListener('scroll', () => {
  const path = window.location.pathname;
  scrollPositions.set(path, { top: scrollContainer.scrollTop, left: scrollContainer.scrollLeft });
});

function restoreScroll(path) {
  const position = scrollPositions.get(path);
  if (position) {
    scrollContainer.scrollTo({ top: position.top, left: position.left, beh * avior: 'smooth' });
  } else {
    scrollContainer.scrollTo({ top: 0, left: 0 });
  }
}

Интеграция с History API

Для SPA с использованием History API (pushState / replaceState) важно сохранять скролл для каждой истории:

window.addEventListener('popstate', (event) => {
  const path = window.location.pathname;
  restoreScroll(path);
});

Это обеспечивает корректное восстановление позиции при нажатии кнопок «Назад» и «Вперед» в браузере.


Советы по оптимизации

  1. Debounce события скролла Для избежания лишних записей в Map при частом скролле рекомендуется использовать debounce:
let scrollTimeout;
window.addEventListener('scroll', () => {
  clearTimeout(scrollTimeout);
  scrollTimeout = setTimeout(() => {
    scrollPositions.set(window.location.pathname, { x: window.scrollX, y: window.scrollY });
  }, 100);
});
  1. Лимитирование Map Для долгих сессий и большого количества маршрутов можно ограничивать размер Map или использовать WeakMap для автоматической очистки.

  2. Учет асинхронного контента Если страница загружается лениво (lazy loading), лучше восстанавливать скролл после того, как все изображения и шрифты будут загружены:

window.addEventListener('load', () => {
  restoreScroll(window.location.pathname);
});

Заключение

Scroll restoration в Universal Router реализуется через комбинацию:

  • сохранения позиции скролла при уходе с маршрута,
  • восстановления позиции после навигации,
  • учета асинхронного рендера и динамических компонентов,
  • интеграции с History API для работы кнопок «Назад/Вперед».

Такой подход обеспечивает естественное поведение навигации и комфортную работу пользователей в SPA.