<- Back to blog
Platform12 min readUpdated May 1, 2026

Next.js Analytics: Tracking Pageviews, Events, Web Vitals

Install analytics on a Next.js App Router app in 2026: next/script in app/layout.tsx, client component event tracking, web vitals via reportWebVitals, and the SPA pageview pitfall.

nextjs analyticsnext.js analyticsapp router analyticsnext/script analyticsnext.js web vitalsnext.js event tracking

TL;DR

  • 1.In App Router, add the script in `app/layout.tsx` using `<Script src="..." strategy="afterInteractive" />` from `next/script`.
  • 2.Vercel Analytics is fast to install but expensive at scale and limited on event tracking. It is not a full GA4 replacement.
  • 3.SPA-style navigation does not fire native pageview events — pick an analytics tool that auto-tracks `history.pushState`, or fire pageviews manually on route change.
  • 4.Track events from client components by calling `window.sleek("track", "name", payload)`. Server components cannot fire events directly.
  • 5.Web vitals: use Next's built-in `useReportWebVitals` to forward LCP / CLS / INP / FCP / TTFB to your analytics tool.

Native Next.js analytics options in 2026

Next.js itself does not ship analytics — it ships hooks that make analytics easy. The two relevant primitives are `next/script` (the right way to load any third-party script) and `useReportWebVitals` (the right way to capture Core Web Vitals).

The closest thing to "native" Next.js analytics is Vercel Analytics, which is a paid product on top of Vercel hosting. It captures pageviews and web vitals automatically, with a clean dashboard. The downsides: it is priced per event, the free tier is small, custom event tracking is limited, and you only really get value if you host on Vercel.

For most teams, the right setup is Next-native primitives plus a privacy-first analytics tool that does not lock you to a host.

Worth flagging the recent breaking changes: this version of Next.js has shifted the conventions for `<Script>` placement, the App Router's server-component boundary affects where you can call `window`, and `useReportWebVitals` has moved between packages between versions. Read the relevant docs in `node_modules/next/dist/docs/` before copy-pasting older guides — the patterns from 2023 will not all work in 2026.

Limitations of Vercel Analytics and other host-bundled tools

Vercel Analytics is fine if you are already paying for Vercel Pro and you want a quick dashboard with zero install. For a serious daily growth review, almost everyone outgrows it.

Cloudflare Web Analytics is the closest equivalent on Cloudflare Pages, and it has a similar profile: free, easy, shallow. Netlify's built-in analytics works at the edge / log layer (different from a client script), which means it captures bots and behaves more like server logs than a user-analytics tool. Pick the host-native option only if you need something free, today, and you are comfortable replacing it later.

  • Vercel Analytics free tier is 2,500 events/month — most production sites blow through that in a day.
  • Custom event tracking is limited to 5 properties per event and a few thousand events per month on cheaper tiers.
  • No real-time view, no AI chat, no revenue tracking, no public dashboards.
  • Locked to Vercel hosting. If you ever move to AWS / Cloudflare / Render / your own infra, the analytics moves with you only as raw data.
  • Web Vitals coverage is good but the rest of the dashboard is closer to "built-in metric" than "analytics tool."

Install in App Router with next/script

For the App Router (the default since Next.js 13.4), the right place to load analytics is `app/layout.tsx`. The `next/script` component renders the script tag with the correct loading strategy and avoids the gotchas of pasting raw `<script>` tags into JSX.

Use `strategy="afterInteractive"` for analytics. This loads the script after the page becomes interactive, which is the right tradeoff: pageviews are captured on the first paint, but the script does not block hydration.

Do not use `strategy="beforeInteractive"` for an analytics tag. That strategy was designed for scripts that absolutely must run before hydration (polyfills, font loaders) and it forces inline blocking. Analytics does not need to block. `lazyOnload` is the other extreme — it waits until the page is idle, which can mean missing the first pageview entirely if the visitor closes the tab quickly.

app/layout.tsx
import Script from 'next/script'
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Built with Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <Script
          src="https://getsleek.io/v1.js"
          data-site={process.env.NEXT_PUBLIC_SLEEK_SITE_KEY}
          strategy="afterInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}
note:Put the site key in `NEXT_PUBLIC_SLEEK_SITE_KEY`. The `NEXT_PUBLIC_` prefix is required for env vars that need to be visible to the browser. The site key is not a secret — it is published in the script tag — but env vars keep it out of your repo.

Pages Router and the older `_app.tsx` pattern

If your project still uses the Pages Router (which is fully supported but not the default), the equivalent install lives in `pages/_app.tsx`.

The Pages Router does fire a route-change event you can subscribe to (`router.events.on("routeChangeComplete", ...)`), which is convenient if your analytics tool requires explicit pageview calls on SPA navigation. Most privacy-first tools handle `history.pushState` automatically, but the hook is there if you need it.

pages/_app.tsx
import type { AppProps } from 'next/app'
import Script from 'next/script'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Script
        src="https://getsleek.io/v1.js"
        data-site={process.env.NEXT_PUBLIC_SLEEK_SITE_KEY}
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  )
}

The SPA pageview pitfall

This is the single biggest gotcha on Next.js analytics. When a user clicks a `<Link>` from `/blog` to `/blog/post`, Next does a client-side navigation — no full page reload, no `<head>` re-execution, no native browser pageview event.

Old GA4 setups missed these navigations entirely, which is why GA4-on-Next.js sites in 2022–2023 always undercounted pageviews by 30–60%. Modern analytics scripts (Sleek, Plausible, Fathom) auto-detect `history.pushState` and fire pageviews automatically. GA4 needs explicit handling, usually through `gtag("event", "page_view", ...)` on route change.

If your analytics tool supports `history.pushState` auto-tracking, you do not need to do anything. If it does not, listen for route changes and fire pageviews manually.

Drop `<RouteTracker />` inside `app/layout.tsx`'s `<body>` and every client-side navigation will fire a pageview. Most privacy-first analytics tools handle this automatically — only add it if your tool requires explicit pageview calls.

components/RouteTracker.tsx (client component)
'use client'

import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'

declare global {
  interface Window {
    sleek?: (action: string, name?: string, payload?: Record<string, unknown>) => void
  }
}

export function RouteTracker() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    const url = pathname + (searchParams?.toString() ? '?' + searchParams.toString() : '')
    window.sleek?.('pageview', url)
  }, [pathname, searchParams])

  return null
}

Track custom events from client components

In App Router, server components cannot fire events directly because they do not run in the browser. Anything event-shaped (button clicks, form submits, scroll depth) needs to live in a client component.

A clean pattern is a thin `track()` helper that wraps `window.sleek` so your callers do not have to know about the global. This also makes it easy to swap analytics tools later without touching every component.

lib/analytics.ts
// Tiny client-side wrapper over the analytics global.
// Safe to call from server components — it just no-ops on the server.

export function track(event: string, payload?: Record<string, unknown>) {
  if (typeof window === 'undefined') return
  if (typeof window.sleek !== 'function') return
  window.sleek('track', event, payload)
}

Calling track() from a button

Once the helper exists, every button or form in a client component can fire an event with one import and one call. Co-locate event names with the components that fire them so you can grep your codebase for "where does the signup_clicked event come from."

app/(marketing)/SignupButton.tsx (client component)
'use client'

import { track } from '@/lib/analytics'

export function SignupButton() {
  return (
    <button
      onClick={() => {
        track('signup_clicked', { location: 'hero' })
        window.location.href = '/signup'
      }}
    >
      Start free trial
    </button>
  )
}

Web Vitals with useReportWebVitals

Next.js has a built-in hook for reporting Core Web Vitals: `useReportWebVitals` from `next/web-vitals`. The hook fires a callback for every Web Vital metric — LCP, CLS, INP, FCP, TTFB — and you forward those to your analytics tool.

This is a meaningful upgrade over a GA4 web-vitals integration: it is built into the framework, it captures real user data (RUM), and the values match what Lighthouse / PageSpeed Insights report.

Render `<WebVitals />` once inside `app/layout.tsx`. You will start seeing real user LCP / CLS / INP numbers within a few minutes — and they are usually 30–50% worse than your local Lighthouse scores, which is the point.

app/web-vitals.tsx (client component)
'use client'

import { useReportWebVitals } from 'next/web-vitals'
import { track } from '@/lib/analytics'

export function WebVitals() {
  useReportWebVitals((metric) => {
    track('web_vital', {
      name: metric.name, // 'LCP' | 'CLS' | 'INP' | 'FCP' | 'TTFB'
      value: Math.round(metric.value),
      id: metric.id,
      rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    })
  })
  return null
}

Pitfalls that bite Next.js sites

  • Server actions cannot fire client events. If your form uses a server action, the success path needs to fire the event in client code (e.g. inside a `useFormStatus`-driven component) or via a useEffect that watches the action result.
  • Middleware does not run in the browser. Do not try to fire events from `middleware.ts`. Events live on the client; `middleware.ts` is for redirects and headers.
  • Static export + analytics is fine, but route-based pageviews need the SPA tracker (above). On `output: "export"` you also need to make sure your env vars are baked at build time.
  • Strict mode double-renders effects in dev. If you fire a pageview from a useEffect in dev, you will see it twice. This does not happen in production builds.
  • CSP headers can block analytics. If you set a strict `Content-Security-Policy`, you need to allowlist your analytics domain in `script-src` and `connect-src`.
warning:Calling `window.sleek` from a server component will throw a build error. Always wrap event calls in a `typeof window !== "undefined"` check or — better — put them in a client component marked with `"use client"`.

Alternatives and what to actually use

For Next.js apps in 2026, the strong picks are: Sleek for the daily growth review with built-in AI chat and revenue tracking, Plausible for the open-source / minimal aesthetic, Fathom for EU-hosted compliance, and Vercel Analytics if you are already deep in Vercel and just want pageviews.

Avoid the temptation to install three of them at once "to compare." It is fine for a week, but every script you add costs hydration time. Pick one and own it.

A reasonable rubric: if your app is a SaaS with a Stripe subscription, Sleek's native revenue tracking saves you from manually tagging purchase events. If your app is a content site with international readers, Fathom's EU hosting is a clean compliance story. If your team has a strong open-source preference and you are happy with a minimal dashboard, Plausible is the right call. If you just shipped your MVP and you only need pageviews for the first month, Vercel Analytics is the lowest-friction install.

  • Sleek — privacy-first with a polished dashboard, AI chat, Stripe revenue tracking
  • Plausible — minimal, open source, very common in the Next.js community
  • Fathom — privacy-first, EU-hosted option for GDPR-sensitive teams
  • Vercel Analytics — fast install if you host on Vercel; expensive past the small free tier
  • PostHog — heavier, includes session replay and feature flags; overkill if you just want analytics

Frequently asked questions

Where do I put analytics in a Next.js App Router app?

In `app/layout.tsx`, using the `Script` component from `next/script` with `strategy="afterInteractive"`. This loads the script on every page after hydration without blocking. For Pages Router, the equivalent place is `pages/_app.tsx`.

Why is my Next.js GA4 undercounting pageviews?

Next.js client-side navigation (the `<Link>` component) does not fire a native browser pageview event. GA4 needs explicit `gtag("event", "page_view")` calls on route change. Privacy-first tools that auto-detect `history.pushState` (Sleek, Plausible, Fathom) handle this without extra code.

Can I use Vercel Analytics as a GA4 replacement?

For pageviews and web vitals on a Vercel-hosted app, yes. For custom event tracking, real-time, AI chat, or anything past the basics, Vercel Analytics is limited and gets expensive quickly. Most teams pair it with another analytics tool or replace it entirely.

How do I track Core Web Vitals in Next.js?

Use the built-in `useReportWebVitals` hook from `next/web-vitals` inside a client component. The hook fires for LCP, CLS, INP, FCP, and TTFB — forward each metric to your analytics tool with `track("web_vital", { name, value, rating })`.

Can I fire analytics events from a server component?

No. Server components run on the server and have no `window` object. Move the event call into a client component (marked `"use client"`), or fire it from a `useEffect` in a client wrapper that consumes the server data.

Does next/script work with privacy-first analytics?

Yes. `next/script` is just a smart wrapper around `<script>`; it works with any third-party tool. Use `strategy="afterInteractive"` for analytics — `lazyOnload` is too late and `beforeInteractive` is too early for an analytics script.

Will analytics slow down my Next.js app?

Privacy-first analytics scripts are 1–3 KB and load async, so the impact on LCP is unmeasurable. GA4 + Meta Pixel + Hotjar together can add 200+ KB and 100+ ms to first interaction. The biggest performance wins on Next.js sites are usually from removing scripts, not adding them.

Track your own growth loop

Sleek Analytics gives you visitors, sources, pages, devices, and real-time behavior with one lightweight script. No cookies, no GDPR banners.

Related reading