- A virtual page view is a synthetic pageview event fired when a SPA changes routes without a full browser reload
- Inspectlet automatically hooks
history.pushState,history.replaceState, and thepopstateevent—no configuration required - Each virtual page view updates the session timeline, generates its own heatmap data, and can serve as a funnel step
- Works out of the box with React Router, Vue Router, Angular Router, Next.js, Nuxt, and SvelteKit
- Advanced controls let you disable automatic detection or override the reported URL for custom routing scenarios
What Are Virtual Page Views?
In a traditional multi-page website, every time a user clicks a link the browser requests a new HTML document from the server. The page unloads, the new page loads, and analytics tools count that as a pageview. It's straightforward because the browser's native navigation model does all the heavy lifting.
Single-page applications work differently. Frameworks like React, Vue, and Angular load a single HTML shell once, then dynamically rewrite the page content using JavaScript as the user navigates. The URL in the address bar changes (thanks to the History API), the content swaps, and the user experiences what feels like page navigation—but the browser never actually loads a new page.
A virtual page view is a synthetic event that bridges this gap. When a SPA changes routes, analytics and recording tools fire a virtual page view to signal "the user just navigated to a new page" even though no traditional page load occurred. Without this, every user session in a SPA would look like a single long pageview regardless of how many screens the user actually visited.
Why Traditional Analytics Breaks on SPAs
If your analytics tool only listens for the browser's native load event, a SPA presents a serious blind spot. Here's what goes wrong:
- Session recordings show one monolithic page. A user who navigated through your homepage, pricing page, and signup flow appears to have stayed on a single page for 8 minutes. You can't filter recordings by page because the tool never saw a page change.
- Heatmaps aggregate everything together. Clicks from your homepage and your product page get merged into one heatmap tied to the initial URL. The data becomes meaningless.
- Funnels can't track SPA steps. If your conversion funnel goes from
/pricingto/signupto/onboarding, a tool that misses virtual page views can't tell where users drop off. - Pageview counts collapse. A site with 50,000 actual page navigations per day looks like it has 8,000 (only the initial hard loads). Bounce rates, pages-per-session, and time-on-page metrics are all wrong.
This isn't a niche problem. The majority of modern web applications are SPAs or use SPA-style navigation for at least part of their user flow. If your analytics tool doesn't handle virtual page views, you're flying blind on most user journeys.
How Inspectlet Handles SPAs Automatically
Inspectlet detects SPA route changes automatically, with zero configuration. When you install the Inspectlet tracking script, it immediately begins monitoring for virtual navigations. There is nothing extra to enable, no plugins to add, no router integration to wire up.
History API Hooks
Under the hood, the Inspectlet tracking script hooks into the three mechanisms that SPAs use to change the URL:
history.pushState()— the primary method SPAs use to navigate forward to a new route. When your framework callspushState(which React Router, Vue Router, Angular Router, and others all do internally), Inspectlet intercepts it and fires a virtual page view.history.replaceState()— used when a SPA replaces the current history entry (e.g., redirects, URL normalization). Inspectlet detects this too.popstateevent — fires when the user clicks the browser's back or forward buttons. Inspectlet listens for this event to capture backward and forward navigation.
Together, these three hooks cover every way a SPA can change the URL. Whether your user clicks a navigation link, gets redirected, or hits the back button, Inspectlet sees it.
What Gets Captured on Each Virtual Navigation
When Inspectlet detects a route change, its internal mechanism kicks in and performs several actions:
- Updates the current page URL — the new route is recorded so all subsequent user activity is attributed to the correct page
- Emits a virtual page marker in the session timeline — when you watch a session recording, you see a clear marker each time the user navigates to a new virtual page
- Sends updated metadata to the recording server — the new page title, viewport dimensions, and URL are all transmitted so your analytics stay accurate
- Begins a fresh interaction context — clicks, scrolls, and form inputs after the virtual navigation are associated with the new page URL
There is a safety cap of approximately 360 virtual page views per session. This prevents runaway loops (such as a broken redirect cycle) from consuming excessive resources, while being more than sufficient for any realistic user session.
Setup: Zero Configuration for Most Frameworks
If your SPA uses the History API for routing (which is the default for all major frameworks), you don't need to do anything beyond installing Inspectlet normally (see the complete setup guide). Add the standard tracking snippet to your application shell, and virtual page view detection works immediately.
Install the Inspectlet snippet once in your application's root HTML (or layout component). It persists across all virtual navigations automatically. You don't need to reinitialize it on route change.
React (React Router)
React Router v6+ uses history.pushState internally for all <Link> components and useNavigate() calls. Inspectlet hooks these calls automatically.
<!-- Add the Inspectlet snippet to your index.html or root layout -->
<!-- That's it. No React-specific setup needed. -->
<!-- Example: React Router navigation that Inspectlet tracks automatically -->
<Link to="/dashboard">Dashboard</Link>
<Link to="/settings/billing">Billing</Link>
<!-- Programmatic navigation is also captured -->
<script>
// Both of these trigger a virtual page view in Inspectlet
navigate('/dashboard');
navigate('/settings/billing', { replace: true });
</script>
Vue (Vue Router)
Vue Router uses history.pushState in its default createWebHistory() mode. All <router-link> navigations and router.push() calls are captured automatically.
<!-- Vue Router navigations Inspectlet captures automatically -->
<router-link to="/products">Products</router-link>
<router-link to="/cart">Cart</router-link>
<script>
// Programmatic navigation — also captured
router.push('/checkout');
router.replace('/checkout/confirm');
</script>
Angular
Angular's router uses history.pushState with the default PathLocationStrategy. All routerLink directives and router.navigate() calls are captured.
<!-- Angular router navigations — automatic virtual page views -->
<a routerLink="/users">Users</a>
<a routerLink="/users/42/profile">Profile</a>
<script>
// Programmatic — also captured
this.router.navigate(['/users', userId, 'profile']);
</script>
Next.js & Nuxt
Both Next.js and Nuxt use the History API for client-side navigation. After the initial server-rendered page load, all subsequent route changes are SPA-style transitions that Inspectlet captures as virtual page views.
<!-- Next.js — Link component triggers virtual page views -->
import Link from 'next/link';
<Link href="/blog/my-post">Read Post</Link>
// Next.js App Router programmatic navigation
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/blog/my-post');
For Nuxt, add the Inspectlet snippet in your nuxt.config head section or in app.vue. All <NuxtLink> navigations are captured automatically.
Any framework that uses the History API for client-side routing works with Inspectlet automatically. This includes SvelteKit, Remix, Gatsby, Astro (in SPA mode), and Ember. If the URL changes via pushState or replaceState, Inspectlet detects it.
Advanced Configuration
Most teams never need these options—the default automatic detection covers the vast majority of use cases. But for apps with unusual routing patterns or specific privacy requirements, Inspectlet provides manual controls.
Disabling Automatic Virtual Page Detection
Some applications update the URL for reasons other than page navigation. A common example is a search page that writes filter parameters to the query string as the user toggles filters: /products?color=red&size=large. Each filter change updates the URL (for shareability), but it's not really a new page.
In these cases, automatic detection would fire a virtual page view on every filter toggle, inflating your pageview counts and fragmenting your heatmap data. To prevent this:
<!-- Disable automatic virtual page detection -->
<script>
window.__insp = window.__insp || [];
__insp.push(['disableVirtualPage']);
</script>
<!-- Then load the Inspectlet snippet as usual -->
<script>
// ... standard Inspectlet tracking code ...
</script>
The disableVirtualPage command must be pushed before the Inspectlet tracking script loads. It tells Inspectlet to skip hooking into the History API, meaning no virtual page views will be detected automatically.
If you disable automatic detection but still want to track certain navigations, you can fire virtual page views manually using the pageUrl API (covered next).
Overriding the Reported URL
The pageUrl command lets you explicitly set the URL that Inspectlet reports for the current page. This is useful in several scenarios:
- Stripping sensitive parameters: If your URLs contain user IDs, tokens, or other data you don't want in your analytics, override the URL with a clean version
- Normalizing dynamic routes: Consolidate
/user/12345/profileand/user/67890/profileinto a single/user/:id/profilefor cleaner heatmap aggregation and custom metric filtering - Custom routing schemes: If your app uses a routing library that doesn't go through the History API, use
pageUrlto manually signal page changes
<script>
// Override the URL Inspectlet reports
__insp.push(['pageUrl', '/products/category']);
// Example: Strip user ID from URL for privacy
// Instead of recording /user/12345/settings
__insp.push(['pageUrl', '/user/*/settings']);
// Example: Signal a page change manually (when auto-detection is off)
function onRouteChange(newPath) {
__insp.push(['pageUrl', newPath]);
}
</script>
Note that pageUrl sets the URL without triggering a virtual page view navigation event. It updates what URL Inspectlet associates with subsequent user activity. If you've disabled auto-detection and want to fully simulate a page change, call pageUrl at each point in your app where a route transition occurs.
Hash-Based Routing Considerations
Some older SPAs use hash-based routing (/#/dashboard, /#/settings) instead of the History API. Hash changes fire the hashchange event but don't go through pushState or replaceState.
If your application uses hash-based routing exclusively, automatic detection won't capture those navigations. The solution is to use the pageUrl API to manually report route changes:
<script>
// For hash-based routing: manually report virtual page views
window.addEventListener('hashchange', function() {
__insp.push(['pageUrl', location.pathname + location.hash]);
});
</script>
That said, most modern frameworks have long since moved to History API-based routing as the default. Hash-based routing is primarily encountered in legacy applications or frameworks configured to avoid server-side URL rewriting.
How Virtual Pages Affect Your Analytics
Virtual page views aren't just a tracking detail—they fundamentally change (and improve) the analytics data you collect from your SPA. Here's how each Inspectlet feature benefits:
Session Recordings Show All Virtual Navigations
When you watch a session replay, virtual page views appear as clear navigation markers in the timeline. You can see exactly when the user moved from /pricing to /signup, how long they spent on each page, and what they did before leaving. Without virtual page views, this same session would be an unstructured 10-minute block with no page context.
You can also filter session recordings by virtual page URL. Want to see every session where a user visited /checkout? Search for that URL and Inspectlet returns all matching sessions—including those where /checkout was reached via SPA navigation rather than a direct page load.
Heatmaps Per Virtual Page
Each virtual page URL gets its own heatmap. Clicks on /dashboard don't bleed into /settings, and vice versa. This gives you accurate, per-page click, scroll, and attention data even in a SPA where technically only one HTML document was ever loaded.
This per-URL separation is critical for understanding user behavior. A click heatmap for /pricing shows which pricing plan users engage with most. A scroll heatmap for /features shows whether users read all the way to the bottom or drop off after the first fold. These insights would be impossible without proper virtual page view tracking.
Funnels with Virtual Page Steps
Virtual page URLs can be used as steps in conversion funnels. A common SPA funnel might look like:
/pricing— viewed pricing page/signup— started registration/onboarding/step-1— began onboarding/dashboard— reached the product
Even though the user never left the original page from the browser's perspective, Inspectlet tracks each virtual navigation and reports accurate funnel drop-off rates at each step.
Search and Filter by Virtual Page URLs
Every filtering and search capability in the Inspectlet dashboard works with virtual page URLs. You can find sessions that visited specific pages, filter form analytics by the page the form appeared on, and break down metrics by virtual page—all the same as if they were traditional page loads.
Troubleshooting Common SPA Tracking Issues
URL Changes Not Detected
If Inspectlet isn't picking up your route changes, the most common causes are:
- Hash-based routing: Your router uses
/#/pathURLs instead of/path. Switch to History API mode in your router configuration, or use thepageUrlAPI to manually report route changes (see the hash-based routing section above). - Custom routing library: If you're using a routing library that manipulates the URL through a non-standard mechanism (not
pushState/replaceState), Inspectlet won't detect those changes. Use__insp.push(['pageUrl', newUrl])to manually report each navigation. - Script loading order: If the Inspectlet snippet loads after the first SPA navigation has already occurred, that initial navigation may be missed. Ensure the Inspectlet snippet is in the
<head>of your root HTML file so it loads before your JavaScript bundle.
Open your browser's developer console on your SPA. Navigate to a few pages, then type history.pushState. If it returns a function (even a wrapped one), the History API is available and Inspectlet can hook into it.
Too Many Virtual Page Views
If your session recordings show dozens of virtual page views from what should be a single page visit, the likely cause is URL changes that aren't really navigations:
- Query parameter updates: Filters, sort options, or search queries that update the URL (e.g.,
/products?page=2&sort=price). Each change fires apushStatecall, which Inspectlet interprets as a new page. - Pagination: Infinite scroll or paginated content that appends page numbers to the URL.
- State persistence: Apps that serialize UI state into the URL for deep-linking purposes.
The fix depends on your preference:
- Option A: Use
__insp.push(['disableVirtualPage'])to disable auto-detection entirely, then use__insp.push(['pageUrl', url])to manually report only the navigations you consider real page changes. - Option B: Modify your app to use
replaceStateinstead ofpushStatefor non-navigation URL updates. This prevents the URL change from being counted as a new history entry, though Inspectlet will still detect it. The cleanest solution is Option A if the noise is significant.
Heatmap Data Split Across Routes
If your heatmap data looks thin because visits are spread across many URL variants (e.g., /product/123, /product/456, /product/789), the issue isn't virtual page views per se—it's that each unique URL generates its own heatmap.
Use the pageUrl API to normalize dynamic segments:
<script>
// Normalize product page URLs for heatmap aggregation
// Instead of /product/123, /product/456, etc.
// Report all as /product/:id
var path = location.pathname;
var normalized = path.replace(/\/product\/\d+/, '/product/:id');
__insp.push(['pageUrl', normalized]);
</script>
This consolidates all product page visits into a single heatmap, giving you statistically meaningful data instead of hundreds of heatmaps with one or two visits each.
Testing Your SPA Tracking Setup
Once you've installed Inspectlet on your SPA, verify that virtual page views are being captured correctly. Here's a systematic approach:
Step 1: Verify the Snippet Is Loaded
Open your browser's developer console and type __insp. If Inspectlet is loaded, this will return an array (or object). If it returns undefined, the snippet isn't loaded yet—check your HTML or build configuration.
Step 2: Navigate Through Your App
Click through several pages of your SPA as a normal user would. Hit the back button a few times. Use your app's navigation menu. Try both links and programmatic navigation (e.g., form submissions that redirect).
Step 3: Check the Inspectlet Dashboard
Wait 1–2 minutes (sessions are processed in near-real-time), then open the Inspectlet dashboard. Find your test session in the session list. When you play it back, you should see:
- Navigation markers in the timeline for each route change
- The correct URL displayed at each point in the recording
- Page transitions reflected in the playback (content changes when the URL changes)
Step 4: Verify Heatmap Separation
Navigate to the heatmaps section of your dashboard and enter a virtual page URL (e.g., /dashboard). If data appears, your virtual page views are being attributed to the correct URLs. Check a second URL to confirm data isn't cross-contaminated.
Step 5: Test Edge Cases
Run through these scenarios to ensure comprehensive coverage:
- Browser back/forward buttons — should trigger virtual page views via the
popstateevent - Direct URL entry — user types a URL directly in the address bar (this is a hard navigation, not a virtual page view, but Inspectlet should still capture it as a normal pageview)
- Page refresh — refreshing on a SPA route should load the page fresh, not count as a virtual navigation
- External link return — user leaves your site, then comes back via browser history
Track Every Page in Your SPA
Inspectlet captures virtual page views automatically. Get accurate session recordings, heatmaps, and funnels for your single-page application.
Framework-Specific Tips
React with Code Splitting
React apps that use React.lazy() and Suspense for route-based code splitting work seamlessly with Inspectlet. The pushState call happens before the lazy component loads, so Inspectlet captures the virtual page view immediately. The recording then shows the loading state followed by the rendered content—exactly what the user experienced.
Server-Side Rendered SPAs (Next.js, Nuxt)
In SSR frameworks, the first page load is server-rendered (a real pageview), and subsequent navigations are SPA-style (virtual page views). Inspectlet handles both correctly. The tracking snippet initializes on the server-rendered page and then captures all subsequent client-side navigations.
One caveat: if you use Next.js getServerSideProps or Nuxt server middleware that causes full page reloads on certain routes, those will be tracked as standard pageviews rather than virtual page views. This is correct behavior—the browser actually loaded a new page.
Micro-Frontends
In micro-frontend architectures where multiple frameworks share a single URL space, install the Inspectlet snippet in the shell application (the one that controls the URL bar). As long as route changes go through the History API, Inspectlet captures them regardless of which micro-frontend initiated the navigation.
Frequently Asked Questions
Do I need to call any Inspectlet API on every route change?
No. If your SPA uses the History API (which all major frameworks do by default), Inspectlet detects route changes automatically. You only need to use the API if you have custom routing needs, hash-based routing, or want to normalize URLs.
What happens if a user visits 100+ pages in a single session?
Inspectlet supports up to approximately 360 virtual page views per session. This limit exists to handle edge cases like redirect loops or automated browsing. For normal user sessions, this limit is never reached.
Does virtual page view tracking affect page load performance?
The History API hooks add negligible overhead. Inspectlet wraps the native pushState and replaceState functions and executes a lightweight callback when they're called. The actual navigation timing is unaffected—Inspectlet's callback runs asynchronously and does not block the route change.
Can I use virtual page views with Inspectlet's form analytics?
Yes. Forms on virtual pages are auto-detected just like forms on traditionally loaded pages. When a user navigates to a virtual page containing a form, Inspectlet identifies the form fields and begins tracking interactions. This works whether the form was present in the initial HTML or dynamically rendered after the route change.
How do virtual page views interact with the page load time feature?
Page load time metrics apply to the initial hard page load (when the browser actually requests a new document). Virtual page views don't have traditional load timing because no new document is fetched. However, the session recording captures the rendering performance of each virtual navigation—you can see how quickly the new content appeared after the route change.