route-transition-tracking
Measure time from navigation to page fully loaded and interactive. Use when tracking SPA navigation, route changes, or slow page transitions.
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for measuring and optimizing Single Page Application (SPA) navigation performance. It tracks the complete transition lifecycle—from the initial navigation trigger to the destination page becoming fully interactive—enabling developers to pinpoint latency in data fetching, rendering, and framework-specific routing. By implementing these patterns, teams can ensure their web applications meet high-performance thresholds and provide a seamless user experience.
Use Cases
- Performance Benchmarking: Measuring route transition times against industry standards (e.g., <400ms) to maintain a high-quality user experience.
- Cross-Framework Implementation: Utilizing standardized tracking logic for React, Next.js, Vue, Svelte, and Nuxt to ensure consistent metrics across diverse tech stacks.
- Bottleneck Identification: Detecting whether slow page loads are caused by route change logic, slow API data fetching, or heavy component rendering.
- Regression Testing: Monitoring navigation speed over time to identify performance regressions introduced by new features or complex code changes.
- User Experience Optimization: Tracking 'warm' vs 'cold' loads and back/forward navigation to optimize caching strategies and perceived performance.
| name | route-transition-tracking |
|---|---|
| description | Measure time from navigation to page fully loaded and interactive. Use when tracking SPA navigation, route changes, or slow page transitions. |
| priority | 2 |
Route Transition Tracking
Time from navigation start to destination page interactive.
Phases
CLICK/NAV → ROUTE_CHANGE → DATA_FETCH → RENDER → INTERACTIVE
|_______________________________________________|
Route Transition Time
When to Use
- Tab/menu navigation
- Link clicks within SPA
- Programmatic navigation
- Back/forward browser buttons
- Deep links
Key Thresholds
| Rating | Duration |
|---|---|
| Good | <400ms |
| Acceptable | <1s |
| Poor | >1s |
React Router
import { useLocation, useNavigationType } from 'react-router-dom';
import { useEffect, useRef } from 'react';
function RouteTracker() {
const location = useLocation();
const navigationType = useNavigationType();
const navigationStart = useRef<number>();
useEffect(() => {
navigationStart.current = performance.now();
}, [location.pathname]);
useEffect(() => {
// Called after render completes
if (navigationStart.current) {
const duration = performance.now() - navigationStart.current;
trackRouteTransition({
from: document.referrer,
to: location.pathname,
duration_ms: duration,
navigation_type: navigationType, // 'PUSH', 'POP', 'REPLACE'
});
}
});
return null;
}
Next.js App Router
// app/providers.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
export function RouteChangeTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
const startTime = useRef<number>();
const previousPath = useRef<string>();
useEffect(() => {
startTime.current = performance.now();
}, [pathname, searchParams]);
useEffect(() => {
if (startTime.current && previousPath.current) {
const duration = performance.now() - startTime.current;
trackRouteTransition({
from: previousPath.current,
to: pathname,
duration_ms: duration,
has_search_params: searchParams.toString().length > 0,
});
}
previousPath.current = pathname;
});
return null;
}
Vue Router
// plugins/route-tracking.ts
import { watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
export function useRouteTracking() {
const router = useRouter();
const route = useRoute();
let navigationStart: number;
router.beforeEach((to, from) => {
navigationStart = performance.now();
});
router.afterEach((to, from) => {
// Wait for next tick to ensure render complete
nextTick(() => {
const duration = performance.now() - navigationStart;
trackRouteTransition({
from: from.path,
to: to.path,
duration_ms: duration,
route_name: to.name as string,
});
});
});
}
SvelteKit
// src/routes/+layout.svelte
<script lang="ts">
import { page, navigating } from '$app/stores';
import { onMount } from 'svelte';
let navigationStart: number;
$: if ($navigating) {
navigationStart = performance.now();
}
$: if (!$navigating && navigationStart) {
const duration = performance.now() - navigationStart;
trackRouteTransition({
to: $page.url.pathname,
duration_ms: duration,
route_id: $page.route.id,
});
}
</script>
Nuxt
// plugins/route-tracking.client.ts
export default defineNuxtPlugin((nuxtApp) => {
let navigationStart: number;
nuxtApp.hook('page:start', () => {
navigationStart = performance.now();
});
nuxtApp.hook('page:finish', () => {
const route = useRoute();
const duration = performance.now() - navigationStart;
trackRouteTransition({
to: route.path,
duration_ms: duration,
route_name: route.name as string,
});
});
});
With Data Loading
Track data fetching as part of transition:
function ProductPage() {
const { data, isLoading } = useQuery(['product', id], fetchProduct);
const renderStart = useRef(performance.now());
useEffect(() => {
if (!isLoading && data) {
trackRouteTransition({
route: `/products/${id}`,
total_duration_ms: performance.now() - renderStart.current,
data_fetch_complete: true,
});
}
}, [isLoading, data]);
// ...
}
Common Mistakes
- Measuring only route change (missing data load)
- Not distinguishing warm vs cold loads
- Ignoring prefetched routes (artificially fast)
- Missing back/forward navigation
- Not tracking by route pattern (aggregate hides issues)
Related Skills
- See
skills/core-web-vitalsfor LCP during navigation - See
skills/hydration-performancefor SSR page loads - See
skills/api-tracingfor data fetch timing - See
skills/user-journey-trackingfor correlating with user intent
References
references/ui-performance.md- Navigation performance patternsreferences/frameworks/*.md- Framework-specific routing