Clycyo
Frameworks7 min read

Next.js Analytics: The Complete Setup Guide

Add privacy-first analytics to a Next.js app — App Router and Pages Router — with SPA route-change tracking, Web Vitals, and zero impact on your Lighthouse score.

Next.js gives you a fast site out of the box; the wrong analytics script is the quickest way to throw that away. This guide sets up privacy-first analytics on Next.js — App Router or Pages Router — with client-side navigation tracked correctly, Web Vitals measured from real users, and a script budget of about 1 KB. This site (clycyo.com) is itself a Next.js static export running exactly this setup, and its numbers are public at /open.

The two Next.js-specific problems

  1. Client-side navigation. After the first load, Next.js swaps pages without full reloads. A naive tracker records one pageview per session and goes blind. Your tracker must hook the History API.
  2. Performance budget. You did not adopt Server Components and code-splitting to spend 100 KB on gtag + GTM. Script weight lands on INP and LCP — the metrics Google ranks you on.

Installation — App Router

Add the script in your root layout. Clycyo's tracker detects SPA route changes automatically (History API patching), so this is genuinely the whole setup:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <script
          defer
          src="https://clycyo.com/tracker.js"
          data-tracking-id="YOUR_TRACKING_ID"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

With defer, the 1.1 KB script downloads in parallel and executes after parsing — it never blocks rendering. Lighthouse impact: none measurable. If you prefer next/script, strategy="afterInteractive" achieves the same.

Installation — Pages Router

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

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        <script
          defer
          src="https://clycyo.com/tracker.js"
          data-tracking-id="YOUR_TRACKING_ID"
        />
      </Head>
      <body><Main /><NextScript /></body>
    </Html>
  );
}

What gets tracked automatically

  • Initial pageviews with full load timing (navigationStart → loadEventEnd).
  • Client-side route changes, with SPA frame-time so slow transitions are visible too.
  • Web Vitals (LCP, CLS, INP) from real visitors — field data, not lab estimates.
  • JavaScript errors with the URL they occurred on.
  • Clicks, referrers, UTM parameters, device and country.

Product events in client components

'use client';

export function UpgradeButton() {
  return (
    <button
      onClick={() => {
        window.webanalytics?.track('upgrade_clicked', {
          plan: 'pro',
          location: 'pricing_page',
        });
      }}
    >
      Upgrade
    </button>
  );
}

The optional chaining matters: the tracker is deferred, ad blockers exist, and your UI should never depend on analytics being present.

identify() after signup

// In your post-signup client logic
window.webanalytics?.identify(user.email);

// And persist the visitor id for server-side revenue webhooks
await api.post('/me', {
  clycyo_visitor_id: window.webanalytics?.getVisitorId(),
});

That second line is what lets a Stripe webhook attach revenue to this user's journey later — the full wiring is in our Stripe guide.

Static export, middleware, and edge cases

  • output: 'export' works unchanged — the tracker is a plain script tag with no server dependency. This site is proof.
  • Streaming/Suspense: the tracker fires on load and route change, not on hydration milestones, so partial rendering does not double-count.
  • No cookie banner needed: cookieless tracking means your Next.js app ships without a consent wall for analytics — one less layout shift, better CLS.

Total setup time is about two minutes; the quickstart has copy-paste snippets for every framework. For the React-specific background on why SPA tracking breaks naive tools, see React SPA Analytics.