Next.js Analytics Without Google Analytics in 2026

One script tag in your root layout, route changes counted automatically, custom events from client components, and real-user vitals. No cookies, no consent banner, no gtag.js in your bundle graph.

By Null Agency · Updated September 23, 2026 · Written for developers shipping Next.js App Router or Pages Router sites

TL;DR: it is one tag, and Next.js does not get in the way

Next.js is a client-side-routed React app, and that is the only thing that trips up analytics. A tool that counts one pageview per full page load will miss most navigations after the first. A tool that is SPA-aware hooks the History API and counts each route change. That is the whole integration problem.

If you are hosted on Vercel and weighing the built-in option, read the Vercel Web Analytics alternatives comparison alongside this one.

Why analytics behaves differently in a Next.js app

A classic multi-page site loads a fresh HTML document on every click. An analytics script initializes, fires one pageview, and is discarded with the page. Next.js does not work that way. After the first document load, the router fetches the next route's payload and swaps the rendered tree in place, updating the URL with the History API. The browser never performs a second full navigation, so the script that loaded once stays alive for the whole visit.

That has three practical consequences for analytics.

The tag must live in the root layout, not in a page. In the App Router, the root layout.tsx persists across navigations and is not remounted. A tag placed there loads once per visit. A tag placed in a page component or a nested layout can be added and removed as the user moves around, depending on where the boundary sits.

Pageview counting must follow the URL, not the load event. If a tool only reports on load, a visitor who lands on your homepage and clicks through four docs pages shows up as one pageview. Tools that patch history.pushState and listen for popstate see all five. GhostMetrics does exactly this, so the Back button and Link clicks both register, with no router hook for you to maintain.

Elements appear after load. Modals, tabs and client-rendered widgets mount long after the initial document. An event tracker that scans the DOM once at startup misses them. GhostMetrics uses one delegated document listener for data-gm-event, so a button rendered by a client component ten navigations later is tracked the same as one in the initial HTML.

The other reason to think about analytics in a Next.js project is weight. A typical Next.js site already ships a framework runtime; adding a Google tag on top is a real cost. We measured gtag.js at about 150 KB compressed, against about 3 KB compressed (10 KB uncompressed) for the GhostMetrics script, which is served from Cloudflare's edge. Neither number includes what your own components add, but one of them is fifty times the other.

Your Next.js analytics options, compared

Every tool below can be used on a Next.js site. The table describes how each one installs, what it does about cookies, and how it is sold. Where a detail changes with plans or over time, check the vendor's current pricing page rather than trusting a blog post, including this one.

ToolHow it installs in Next.jsCookies / consentCustom eventsPricing modelBest fit
GA4gtag snippet, or the @next/third-parties Google Analytics componentSets _ga cookies by default; consent generally required in the EU/UKYes, plus audiencesFree tier; BigQuery export is freeGoogle Ads attribution and ecommerce funnels
Vercel Web Analytics@vercel/analytics package, read in the Vercel dashboardCookieless; visitors identified by a daily-resetting hashNot on Hobby; custom events on ProPer collected event; Hobby includes 50,000 events a monthVercel-only projects that want zero setup
PlausibleScript tagCookieless, EU-only processingYes (goals)No free plan; 30-day trial; Starter from $9/month for 10k pageviewsOpen source or EU-residency requirements
FathomScript tagCookielessYesPageview-tiered; from $15/month for 100,000 pageviews; 7-day trialMany sites on one plan
UmamiScript tagCookielessYesFree to self-host (you pay for infra); hosted Umami Cloud also offered, check current pricingTeams that want to run their own stack
GhostMetricsScript tagCookieless, no localStorage, no stored IPsYes (data-gm-event or track())Free for one site; Pro $9.99/mo flatOne Next.js site with events and vitals, no server

Three notes on the rows. GA4 is genuinely the right answer if you buy Google Ads and need conversion attribution or ecommerce funnels; nothing on this list replaces that. If GA4 is where you are today, our Google Analytics alternatives page covers the exit criteria. Vercel Web Analytics is convenient, but it is priced per event and only exists on Vercel; the comparison here walks through where it stops. Plausible and Umami are open source, which GhostMetrics is not. If reading the source is a hard requirement, pick one of those.

App Router: add the tag to the root layout

This is the whole integration for a Next.js 13+ project using the app/ directory. The root layout is a server component by default, and a plain <script> element inside <head> is valid there.

  1. Create a site in GhostMetrics. Sign up on the free plan and add your domain. You get a site ID and a one-line snippet.
  2. Open app/layout.tsx. This is the file that exports RootLayout and renders <html> and <body>.
  3. Add the tag inside <head>. Use your own site ID in data-site.
  4. Deploy and load a page. Open the site in a fresh tab, then check the real-time view in the dashboard. Your own visit should appear within seconds.
  5. Click through two or three routes. Each client-side navigation should add a pageview without a full reload. This confirms the SPA behavior is working in your app.
// app/layout.tsx
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <head>
        <script
          defer
          src="https://ghostmetrics.nullagency.io/gm.js"
          data-site="YOUR-SITE-ID"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

A plain script element is what we show because it is the closest thing to the tag GhostMetrics documents, and it keeps data-site exactly as written. next/script also works if you prefer its loading strategies; either way the tag lives in the root layout and loads once. There is no useEffect, no provider, and no usePathname hook to write, because the script observes the History API itself.

One environment note. The tag runs in every environment that serves the layout, including local development and preview deployments, so those visits land in your dashboard too. If you would rather keep them out, render the element conditionally.

{process.env.NODE_ENV === 'production' && (
  <script
    defer
    src="https://ghostmetrics.nullagency.io/gm.js"
    data-site="YOUR-SITE-ID"
  />
)}

That guard only excludes next dev. Preview deployments are production builds, so if you want them out too, gate on your host's environment variable for previews, or use a separate site ID for previews.

Pages Router: use _document.tsx

If your project still uses pages/, the equivalent of the root layout head is pages/_document.tsx. It is rendered only on the server (per request, or at build time for static pages) and wraps every page.

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script
          defer
          src="https://ghostmetrics.nullagency.io/gm.js"
          data-site="YOUR-SITE-ID"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Do not put the tag in _app.tsx via next/head. That works for many things, but _document is the right home for a tag that should be present on every document and never re-evaluated. As with the App Router, route changes made by next/link and router.push() use the History API and are counted as pageviews automatically.

If your site has a Content-Security-Policy header, allow the ghostmetrics.nullagency.io origin for scripts and for the beacon requests the tracker sends. A blocked tracker fails silently: pages load fine and the dashboard stays empty, which is easy to mistake for a slow start.

A mixed project, with some routes in app/ and some in pages/, needs the tag in both places or, more simply, only in whichever entry point renders the document for a given route. Check both in the real-time view after deploying. The same tag pattern applies to any framework; our guide to adding analytics to a static site has the Astro, Hugo and 11ty variants.

Custom events from JSX and client components

Pageviews tell you what people read. Events tell you what they did: clicked the signup button, opened the pricing modal, copied the install command. GhostMetrics gives you two ways to record them, and both fit how Next.js renders.

Declarative: a data attribute in JSX

Add data-gm-event to any element and clicks on it are recorded under that event name. It works in server components too, because the attribute is just HTML in the output. Nothing needs 'use client'.

// app/pricing/page.tsx (a server component)
export default function Pricing() {
  return (
    <a href="/signup" data-gm-event="pricing-signup-click">
      Start free
    </a>
  );
}

Because there is one delegated listener on the document, this also covers elements that a client component renders after hydration, after a route change, or after a state update. You do not re-bind anything when the tree changes.

Programmatic: window.ghostmetrics.track

For events that are not clicks, such as a form that validated and submitted, call track() from a handler in a client component. Only the section property is stored as metadata. Each event carries the page path and any UTM values from the session.

'use client';

export function NewsletterForm() {
  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    // ...submit to your API...
    window.ghostmetrics?.track('newsletter-subscribe', { section: 'footer' });
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>
  );
}

If you use TypeScript, the window.ghostmetrics property does not exist on the default Window type, so add one declaration file.

// types/ghostmetrics.d.ts
export {};

declare global {
  interface Window {
    ghostmetrics?: {
      track: (name: string, meta?: { section?: string }) => void;
    };
  }
}

The optional chaining in the form handler is deliberate. The script is loaded with defer, and a visitor with Global Privacy Control or Do Not Track enabled gets nothing collected, so window.ghostmetrics may be absent in either case. Your form should submit whether or not analytics ran.

Be aware of the limits. Events have a name, a count, a last-fired time, the page path and the session UTM. There is no numeric value or revenue field, and no per-visitor funnel across sessions. For revenue, keep using your payment provider's dashboard. For a fuller treatment of event naming, see how to track custom events without Google Analytics. And if you send traffic from campaigns, the UTM parameters guide covers how to tag links so the source shows up in the UTM Breakdown panel.

Core Web Vitals from real Next.js visitors

Next.js developers tend to care about performance, and lab scores from a laptop on fast Wi-Fi only tell part of the story. GhostMetrics captures four vitals from real visitors' browsers using PerformanceObserver and Navigation Timing: FCP, LCP, CLS and TTFB. The dashboard has a Core Web Vitals panel with a per-page table, so you can see which routes are slow for actual users rather than which one scored badly in a single test run.

INP is not captured. If interaction latency is a concern for your app, and for a client-heavy Next.js UI it may well be, you will need Chrome's own tooling or your own instrumentation for that metric.

One definition worth knowing, since Next.js sites usually sit behind a CDN and often behind redirects. GhostMetrics now measures TTFB the way Google does: responseStart measured from navigation start. That includes redirects, DNS, TCP and TLS, and server time, and it is rated against the 800 ms "good" threshold. So a trailing-slash redirect or a www to apex redirect counts against TTFB, as it does in Google's numbers. Rows recorded by the older tracker, which measured server time only, are excluded from the dashboard average and stay in the raw export, labelled in the manifest.

Practical use: check the per-page table after a release. If the LCP for a route moves, look at what changed in that route, such as a hero image that lost its priority prop or a data fetch that moved from a server to a client component. For the broader method, see Core Web Vitals real user monitoring.

Consent: what changes when you drop Google

The cookie banner is usually the visible reason teams leave GA4. GA4 sets _ga cookies by default, and consent is generally required for cookie-based analytics in the EU and UK. That means a banner component, a consent state store, and code in your layout that delays loading the Google tag until the visitor answers, which in turn means the visitors who decline are missing from your numbers.

GhostMetrics sets no cookies and uses no localStorage. It stores no IP addresses (they are hashed), holds no personal data, and creates no identifier that follows a visitor across sessions. The only browser storage it touches is per-tab sessionStorage, which holds a scroll-depth counter and the first UTM set seen in that tab, and is cleared when the tab closes. It honors Global Privacy Control and Do Not Track by collecting nothing. For a Next.js team, the practical outcome is that you can delete the consent provider that wraps your tree, or at least stop making analytics depend on it.

Whether you still need a banner for other reasons, such as ad pixels or embedded video, depends on everything else your site loads. See do I need a cookie banner and web analytics without a consent banner for the details, and get legal advice for your own situation.

Migrating an existing Next.js site off GA4

If your app already uses @next/third-parties or a hand-rolled gtag component, the switch is small in code and mostly a matter of order.

  1. Export what you need from GA4 first. Event-level data in standard properties is retained for 2 or 14 months, depending on settings. Take what you want before you remove anything.
  2. Add the GhostMetrics tag alongside GA4 in the root layout and deploy.
  3. Run both for a couple of weeks. Counts will not match exactly. Cookieless tools count sessions differently, and visitors who declined your banner never appeared in GA4.
  4. Map your GA4 events. Add data-gm-event attributes or track() calls for the handful of events you actually use.
  5. Remove the Google component and the consent wiring. Delete the GA component from the layout, drop the environment variable for the measurement ID, and remove the banner if analytics was its only job.

What you give up is real: Google Ads attribution, audiences, ecommerce funnel reports, and multi-session user paths. GhostMetrics attributes UTMs by first touch within a tab session and has no returning-visitor concept by design. If those capabilities drive revenue for you, keep GA4 and consider our cookieless analytics comparison for a second tool.

Next.js analytics with one tag and no consent plumbing

Cookieless$0 forever1 site free$9.99/mo flat Pro

GhostMetrics is cookieless web analytics by Null Agency, and we run it on this site. Paste one script tag into your root layout or _document.tsx and you are live; there is no package, no tag manager, no build step. Client-side route changes are counted as pageviews, and events work from JSX with data-gm-event or from code with window.ghostmetrics.track(). The free plan is free forever: one site, unlimited pageviews, real-time dashboard, every view, full history.

Running several Next.js projects or want a public dashboard URL? Pro is $9.99/mo flat with unlimited pageviews and unlimited websites (fair-use ceiling of 250), with a 30-day free trial: a card starts the trial, nothing is charged for 30 days, and you can cancel anytime. Raw event export as CSV or JSON is on every plan, including free.

Start free, no card See the live demo

FAQ

Do I need next/script to add analytics to Next.js?
No. A plain script element with defer in the root layout head works, and GhostMetrics documents that form. next/script also works if you want its loading strategies. Either way the tag loads once for the whole app, because the root layout is not remounted on client-side navigation.
Are client-side route changes counted as pageviews?
Yes. GhostMetrics is SPA-aware: a pushState or popstate navigation is recorded as a new pageview, which is how the Next.js App Router and Pages Router change routes on the client. You do not wire up a router hook.
Does GhostMetrics capture INP for my Next.js app?
No. It captures FCP, LCP, CLS and TTFB from real users. INP is not captured, so keep using Chrome's tooling or your own instrumentation if you need it.
Can I track button clicks from a React client component?
Yes, two ways. Add a data-gm-event attribute to the element in your JSX, or call window.ghostmetrics.track('name', { section: 'pricing' }) from an event handler. Only the section property is stored as metadata, and there is no numeric value or revenue field.
Is the free plan enough for a Next.js site?
For one site, yes: the free plan has unlimited pageviews, the real-time dashboard, every dashboard view, full history and raw CSV or JSON export. Pro is $9.99/mo flat and adds unlimited websites (fair-use ceiling of 250) and a public shareable dashboard URL.

Keep reading

Vercel Analytics Alternatives

Where the built-in option stops, and what to use instead

Add Analytics to a Static Site

Snippets for Astro, Hugo, 11ty, Next.js and more

Core Web Vitals RUM

Measure vitals from real visitors, not lab runs

GhostMetrics Live Demo

The real dashboard, live data

Disclosure: GhostMetrics is built by Null Agency and we use it on this site, so we are not a neutral party about our own product. We have kept the claims to what the free and paid plans actually do, and pointed to GA4, Plausible, Umami and others where they fit better. Pricing for third-party products can change; always check the vendor's current pricing page before deciding.