Single-page apps
What tracks automatically in a client-side-routed app, what doesn't, and the documented limitation around Web Vitals on route changes.
mrkr detects client-side route changes automatically for essentially every History-API router. One signal, Web Vitals, is a load-time measurement by nature and doesn't restart on a virtual page change: that's a known limitation, not a bug.
What works automatically
The tracker patches both history.pushState and history.replaceState, and listens for popstate, so a route change made through any of the three fires a fresh pageview with no code changes on your part. Patching replaceState matters more than it sounds: Next.js App Router, router.replace(), React Router's { replace: true }, and every query-param state library navigate that way. A change that leaves the path and query string identical is ignored, so a router that replaces state without moving doesn't produce a duplicate pageview. Along with the new pageview:
- Duration, visible time, and engaged time are measured per route: the outgoing route's totals are closed out and flushed as its final
page_leavebefore the new route's timers start, so each route gets its own numbers rather than the last one absorbing the whole visit. - Custom events and everything else work exactly as they do on a traditional multi-page site: none of it depends on a full page load.
- Back/forward through the bfcache fires a pageview too. A restored page would otherwise produce no pageview at all, which inflates bounce rate and deflates pages per session. A bfcache restore continues the existing session rather than starting a new one.
What doesn't reset automatically
One signal is tied to the page's actual load event, not to virtual navigation, and the tracker doesn't restart it on pushState:
- Web Vitals (LCP, CLS, INP, FCP, TTFB) are load-time measurements: they're captured once for the real page load and reported when the tab is hidden or closed, but they don't re-measure for each client-side route. In practice this is usually fine: these metrics describe how the page performed loading in the first place, which a virtual route change doesn't repeat anyway. It does mean a long-lived SPA session reports one set of vitals for the entry route, not one per screen.
Routers that don't use the History API
Detection is hooked to pushState, replaceState, and popstate. A router that navigates purely by changing the URL fragment (an old-style #/route hash router) does not go through any of them, so it won't trigger an automatic pageview: listen for hashchange yourself and call mrkr.pageview() on navigation in that case.