Как изменить URL без перезагрузки страницы: оптимизация и производительность

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему менять URL без перезагрузки — это суперсила

Представьте: вы открываете современный веб-сервис — скажем, почтовый клиент или онлайн-редактор документов. Вы переключаетесь между папками, открываете письма, меняете настройки — и страница не перезагружается. При этом адресная строка браузера меняется, кнопки «Назад» и «Вперёд» работают, а закладки сохраняют нужное состояние. Как это работает? Магия модификации URL без перезагрузки страницы.

Это не просто «фишка» — это основа производительных одностраничных приложений (SPA). Когда-то разработчики использовали хэши (#), но это было некрасиво и неудобно. Сегодня у нас есть History API — мощный инструмент, который позволяет менять URL мгновенно, без мерцания, без потери состояния, без лишних HTTP-запросов.

В этой статье мы разберём, как работает pushState и replaceState, как они влияют на производительность, и как не наступить на грабли при оптимизации. Поехали!

Основы: History API и его методы

Спецификация HTML5 подарила нам интерфейс window.history. Два главных метода для изменения URL без перезагрузки — pushState и replaceState.

pushState — добавляем новую запись в историю

pushState создаёт новую запись в истории браузера. Пользователь может нажать «Назад» и вернуться к предыдущему URL.

// Синтаксис
history.pushState(state, title, url);

// Пример
history.pushState({ page: 1 }, 'Главная', '/home');

Параметры:

  • state — объект данных, связанный с этой записью. Можно хранить любые сериализуемые данные (например, состояние фильтра, ID элемента).
  • title — заголовок страницы (большинство браузеров игнорирует, но лучше передавать пустую строку или осмысленный текст).
  • url — новый URL. Он должен быть относительным или абсолютным, но в пределах того же origin (протокол + домен + порт).

replaceState — заменяем текущую запись

replaceState изменяет текущую запись в истории, не создавая новой. Пользователь не сможет вернуться «назад» к предыдущему URL — он будет заменён.

// Синтаксис
history.replaceState(state, title, url);

// Пример — обновляем URL при изменении фильтра
function applyFilter(filterName) {
  history.replaceState({ filter: filterName }, '', `?filter=${filterName}`);
  updateUI(filterName);
}

popstate — слушаем изменения истории

Когда пользователь нажимает «Назад» или «Вперёд», браузер генерирует событие popstate. Важно: pushState и replaceState не вызывают это событие — его нужно обрабатывать отдельно.

window.addEventListener('popstate', function(event) {
  if (event.state) {
    // Восстанавливаем состояние по данным из event.state
    restorePageState(event.state);
  } else {
    // Если state пустой — возможно, пользователь вышел за пределы нашего приложения
    loadDefaultState();
  }
});

Оптимизация производительности: что важно знать

Модификация URL — это быстро. Но есть нюансы, которые могут превратить суперсилу в источник багов.

1. Не вызывайте pushState слишком часто

Каждый вызов pushState добавляет запись в историю. Если вы вызываете его на каждое движение мыши или каждый символ в поиске — история раздуется, и пользователь будет нажимать «Назад» 100 раз, чтобы выйти из фильтра. Используйте replaceState для промежуточных состояний, а pushState — для логически завершённых действий (например, открытие новой страницы).

2. Синхронизируйте URL и состояние приложения

Не допускайте рассинхронизации: URL показывает одно, а интерфейс — другое. Это убивает UX. Используйте единый источник истины (например, store в Redux или простой объект состояния), который обновляется вместе с URL.

// Плохо: URL обновляется, а UI — нет
history.pushState({}, '', '/products/123');
// ...а данные не загрузились

// Хорошо: сначала меняем UI, потом URL
async function navigateToProduct(id) {
  const product = await fetchProduct(id);
  renderProduct(product);
  history.pushState({ productId: id }, product.name, `/products/${id}`);
}

3. Используйте replaceState для предотвращения дублирования

Если пользователь несколько раз нажимает одну и ту же кнопку — не плодите записи. Замените текущую.

// Пользователь выбирает категорию "Ноутбуки" — первый раз
history.pushState({ category: 'laptops' }, '', '/category/laptops');

// Он же выбирает ту же категорию снова — заменим, чтобы не дублировать
history.replaceState({ category: 'laptops' }, '', '/category/laptops');

4. Обрабатывайте popstate правильно

Многие забывают, что popstate не даёт вам новый URL напрямую — только объект state. Получить текущий URL можно через location.pathname и location.search. Используйте это для восстановления состояния.

window.addEventListener('popstate', function(event) {
  const currentPath = window.location.pathname;
  const searchParams = new URLSearchParams(window.location.search);
  
  // Восстанавливаем состояние на основе URL
  loadStateFromURL(currentPath, searchParams);
});

Реальный пример: Построение простого SPA-роутера

Давайте соберём маленький, но рабочий пример — роутер для одностраничного приложения. Он будет менять URL, подгружать контент и корректно обрабатывать кнопки «Назад» и «Вперёд».

// Простой SPA-роутер
class Router {
  constructor(routes) {
    this.routes = routes;
    this.currentPath = '';
    
    // Обрабатываем нажатие "Назад"/"Вперёд"
    window.addEventListener('popstate', (event) => {
      this.handlePopState(event);
    });
    
    // Загружаем начальный маршрут
    this.loadInitialRoute();
  }

  // Переход на новый маршрут
  navigate(path) {
    // Если это уже текущий маршрут — не делаем pushState
    if (path === this.currentPath) return;
    
    const route = this.routes[path];
    if (route) {
      // Обновляем контент
      this.renderContent(route);
      // Добавляем запись в историю
      history.pushState({ path: path }, '', path);
      this.currentPath = path;
    }
  }

  // Обработка popstate
  handlePopState(event) {
    const path = event.state ? event.state.path : window.location.pathname;
    const route = this.routes[path];
    if (route) {
      this.renderContent(route);
      this                

Блоги

Рекомендаций по книгам