Мягкие навигации в SPA и их обработка

Single Page Application (SPA) характеризуются тем, что переход между «страницами» внутри приложения не сопровождается полной перезагрузкой документа. Такой подход улучшает пользовательский опыт за счёт скорости, но создаёт особенности для метрик Web Vitals, которые традиционно рассчитываются на полной загрузке страницы.

Определение мягкой навигации

Мягкая навигация — это изменение состояния приложения (маршрута, контента) без полной перезагрузки окна браузера. В SPA она реализуется через:

  • History API (pushState, replaceState, popstate)
  • Маршрутизаторы фреймворков (React Router, Vue Router, Angular Router)

Главная особенность мягкой навигации — DOM изменяется динамически, поэтому стандартные события загрузки (load, DOMContentLoaded) не срабатывают повторно. Метрики Web Vitals (LCP, CLS, FID) требуют отдельного внимания, чтобы корректно измерять показатели на каждой новой «странице».

Подключение библиотеки Web Vitals

import { getCLS, getFID, getLCP, getTTFB, getFCP } from 'web-vitals';

Эти функции принимают колбэк, который вызывается при изменении соответствующей метрики:

getLCP(metric => {
  console.log('LCP:', metric.value);
});

В SPA это нужно делать не только один раз при загрузке приложения, но и при каждой мягкой навигации.

Отслеживание LCP при мягкой навигации

Largest Contentful Paint (LCP) измеряет время, когда на экране появляется наибольший элемент контента.

В SPA после перехода на новый маршрут элемент LCP может появиться через некоторое время, поэтому требуется перезапуск измерения LCP на каждом новом маршруте. Например:

function trackLCP() {
  let lcpMetric;
  getLCP(metric => {
    lcpMetric = metric;
    console.log('LCP текущей страницы:', lcpMetric.value);
  });
  return () => {
    // Очистка наблюдателей перед новой навигацией
    lcpMetric = null;
  };
}

// Использование с маршрутизатором
router.afterEach(() => {
  const stopTracking = trackLCP();
  // Опционально: остановка через таймаут
  setTimeout(stopTracking, 5000);
});

Обработка CLS в SPA

Cumulative Layout Shift (CLS) оценивает визуальные сдвиги элементов. В SPA CLS может накапливаться не только на загрузке, но и при динамических вставках контента после мягкой навигации.

  • Необходимо сбрасывать CLS при переходе на новый маршрут.
  • Для этого библиотека web-vitals поддерживает передачу reportAllChanges:
getCLS(metric => {
  console.log('CLS текущей страницы:', metric.value);
}, { reportAllChanges: true });

Таким образом можно получать отдельные CLS-значения для каждой «страницы», а не суммарные по всей сессии.

Обработка FID и других интерактивных метрик

First Input Delay (FID) и Interaction to Next Paint (INP) зависят от интерактивности страницы. При мягкой навигации:

  • Необходимо инициализировать наблюдателей заново после каждого перехода.
  • Если на новом маршруте появляются интерактивные элементы, FID будет рассчитываться корректно только при повторной регистрации обработчиков.
getFID(metric => {
  console.log('FID новой страницы:', metric.value);
});

Для SPA часто используют комбинацию getFID и getINP, чтобы учитывать все пользовательские взаимодействия после динамической загрузки контента.

Интеграция с маршрутизатором

Пример для React Router:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { getLCP, getCLS, getFID } from 'web-vitals';

export default function WebVitalsTracker() {
  const location = useLocation();

  useEffect(() => {
    const stopLCP = getLCP(metric => console.log('LCP:', metric.value));
    const stopCLS = getCLS(metric => console.log('CLS:', metric.value));
    getFID(metric => console.log('FID:', metric.value));

    return () => {
      stopLCP();
      stopCLS();
    };
  }, [location.pathname]);

  return null;
}
  • Каждый раз при изменении location.pathname наблюдатели LCP и CLS перезапускаются.
  • FID можно оставить один раз, так как он измеряется при первом взаимодействии пользователя с документом.

Важные практики

  • Очистка предыдущих наблюдателей — обязательна, чтобы данные LCP/CLS не смешивались между маршрутами.
  • Тайм-ауты и debounce — полезны, если контент на странице подгружается асинхронно (например, изображения, API-запросы).
  • Сегментация метрик по маршрутам — хранение показателей LCP/CLS/FID отдельно для каждого маршрута упрощает анализ производительности.

Итоговый подход

  1. Подключить web-vitals.
  2. На каждый маршрут SPA запускать getLCP и getCLS заново.
  3. FID/INP можно запускать глобально, но при сложных интерактивных компонентах лучше пересчитывать после рендеринга.
  4. Хранить метрики в отдельной структуре для аналитики, привязанной к маршруту.

Такой подход обеспечивает точное измерение Core Web Vitals в приложениях с мягкой навигацией и позволяет выявлять проблемы производительности на каждом этапе пользовательского пути.