Measuring a single-page app (SPA)
Why SPAs escape classic measurement, how to track their view changes without tracking the user, and the pitfalls to avoid.
Contents
Single-page apps — React, Vue, Svelte, Angular and the rest — have changed how sites load. Instead of asking the server for a new page on every click, they rewrite the content directly in the browser. The experience is smoother, but it confuses measurement tools designed for a web made of full reloads. The result: a poorly instrumented SPA gives the illusion that visitors only ever view a single page. Let’s see why, and how to measure a SPA correctly without sacrificing privacy.
Why a SPA distorts classic measurement
Historic audience measurement rests on a simple signal: the loading of a page. Each time the browser fetches a new HTML document, the measurement script runs and records a view. This model fits a traditional multipage site perfectly, where navigating means reloading.
In a SPA, this signal disappears. The first load does fetch a document, but every subsequent navigation — going from the home page to a product page, opening a tab, filtering a list — happens without a new page request. The script runs only once, at startup. Everything after that is invisible to it. You end up with an artificially low page-view count and an incoherent visit duration, simply because the tool does not “see” the user changing screens.
The role of history events
The good news is that a SPA’s navigations are not really silent: they go through the browser’s History API. When the app changes screen, it calls pushState (or replaceState) to update the URL without reloading, and the popstate event signals back-navigations. These calls are the hook for correct measurement.
Tracking a SPA therefore means listening to these transitions and recording a view on every meaningful URL change, exactly as a reload would have done on a classic site. The URL remains the key: it identifies the screen viewed, feeds the most-viewed-pages report, and lets you reconstruct a journey.
How Takt tracks a SPA’s views
Takt’s script is designed for this model. Beyond the initial view, it hooks into the History API transitions and automatically records a new view on every client-side route change. Concretely, once the snippet is in place, a SPA is measured like a multipage site: every screen counts, with no extra configuration in most cases.
For frameworks, the official wrappers integrate with the router and trigger measurement at the right moment, without you having to wire up history events by hand. And when an interaction deserves tracking without changing the URL — submitting a form, opening a key modal — an explicit event call via takt() lets you record it as a named action.
The pitfalls to avoid
Three traps come up often. The first: counting the first view twice, by letting automatic tracking record the initial load while also recording it by hand. A single source of truth is enough; let the script handle the initial view.
The second: measuring meaningless URL changes. A SPA sometimes updates the URL for technical reasons — sort parameters, anchors, a filter’s state — that do not correspond to a new screen for the user. Recording each of these micro-changes inflates page views without teaching you anything. Better to count only the transitions that genuinely change content.
The third: forgetting that localhost is excluded by default. In development, the absence of data does not mean the instrumentation is broken; test on a real domain, or with a script build that does not exclude localhost.
In summary
A SPA does not emit a reload on every navigation, which makes the whole journey after the first screen invisible to classic tools. The solution is not to go back to multipage, but to listen to History API transitions and record a view on every meaningful route change. Takt’s script and its wrappers do this work automatically, and takt() lets you name the interactions that matter. By avoiding double counts and spurious URL changes, you get from a SPA a measurement as faithful as that of a multipage site — and still without cookies or individual tracking.
Take the next step
Measure your audience without a consent banner.
See Takt in action, then install cookieless analytics on your site.
Read next
Symfony analytics without cookies or a consent banner
Cookieless analytics in a Symfony 6.4/7.x app: the Twig function, server-side conversions, and the Turbo placement trap that multiplies pageviews.
August 1, 2026· 9 min read
GuidesAdding analytics to a SvelteKit app without a cookie banner
Adding cookieless analytics to SvelteKit: an SSR-safe component in the layout, events via useTakt, and why you see nothing in development.
August 1, 2026· 9 min read
GuidesAdding analytics to a Laravel app without a cookie banner
Cookieless analytics in Laravel 10/11/12: the Blade directive, server-side conversion tracking, and the attribution trap in queued jobs.
August 1, 2026· 9 min read
Stay in time
An occasional email on privacy, compliance and Takt news. No tracking, no spam.
Your email is only used to send the newsletter, never sold.