Blog

How to add analytics to a Next.js app

Adding analytics to a Next.js application should be simple — especially if you are just getting started with website analytics. In practice, it involves decisions about script loading strategy, App Router compatibility, hydration behavior, route change detection, and performance impact. This guide covers the main approaches — from a simple script tag to full SDK integration — and explains the trade-offs of each.

The simplest approach: a script tag

The most straightforward way to add analytics to any Next.js app is a script tag in your root layout. This works with the App Router, the Pages Router, and any version of Next.js.

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <script
          defer
          src="https://srcbeam.com/sb.js"
          data-site="YOUR_SITE_ID"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

This approach has several advantages. The script loads once and persists across client-side navigations. The defer attribute ensures it does not block rendering. There is no npm package to install, no build step dependency, and no risk of the analytics SDK being tree-shaken away or excluded from the client bundle. It works identically in development and production.

Privacy-focused analytics tools like sourcebeam, Plausible, and Fathom are designed for this approach. Their scripts are small (sourcebeam is under 1 KB, Plausible is around 1.5 KB), load asynchronously, and automatically detect client-side route changes in single-page applications by listening to the History API.

Using next/script

Next.js provides a Script component from next/script that offers more control over script loading behavior. It supports three strategies:

beforeInteractive — loads the script before hydration. Use this only for critical scripts that must execute before any user interaction. Analytics scripts should never use this strategy — it blocks hydration and hurts performance.

afterInteractive (default) — loads after the page becomes interactive. This is equivalent to placing a script tag without defer. Suitable for analytics, but slightly less optimal than lazyOnload.

lazyOnload — loads during browser idle time, after everything else. The lowest-priority option. Good for analytics scripts where a few seconds of delay in the initial pageview is acceptable.

// app/layout.tsx
import Script from 'next/script'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://srcbeam.com/sb.js"
          data-site="YOUR_SITE_ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}

For lightweight analytics scripts (under 5 KB), next/script provides marginal benefit over a plain <script defer> tag. The plain tag is simpler and has fewer moving parts. Use next/script when you need the onLoad callback or when working with heavier scripts like Google Analytics where loading strategy significantly impacts performance.

The npm package approach

Some analytics tools provide npm packages — @vercel/analytics, posthog-js, mixpanel-browser. These are JavaScript SDKs that you import and initialize in your application code.

// app/providers.tsx
'use client'

import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import { useEffect } from 'react'

export function PHProvider({ children }) {
  useEffect(() => {
    posthog.init('YOUR_KEY', {
      api_host: 'https://app.posthog.com',
    })
  }, [])

  return (
    <PostHogProvider client={posthog}>
      {children}
    </PostHogProvider>
  )
}

The npm approach has trade-offs that are worth understanding:

Bundle size impact.The analytics SDK becomes part of your client bundle. PostHog's SDK adds 60+ KB gzipped. Mixpanel adds 30+ KB. This JavaScript is downloaded, parsed, and executed on every page load — it is not a separate resource that can be cached independently of your application code.

Build coupling. Your analytics is now a dependency in your package.json. SDK updates can introduce breaking changes. Version conflicts with other dependencies are possible. If the SDK has a bug, your application is affected until you update.

Client component requirement. Analytics SDKs use browser APIs and React hooks, which means they must run in Client Components (marked with 'use client'). In Next.js App Router, this means wrapping your layout in a provider component, which forces the entire subtree to hydrate on the client. This can negate the performance benefits of Server Components if not structured carefully.

Route change detection. npm SDKs need to hook into Next.js's router to detect navigation events. This typically requires usePathname() and useSearchParams() from next/navigation, called inside a useEffect. Script-tag-based analytics tools handle this automatically by listening to the History API, with zero code from you.

Route change tracking in single-page apps

Next.js uses client-side routing — when a user clicks a link, the page does not fully reload. Instead, Next.js fetches the new route and updates the DOM. This means a traditional analytics script that only fires on page load would only capture the initial pageview.

There are two ways to handle this:

Automatic History API detection. Modern analytics scripts (sourcebeam, Plausible, Fathom) listen for pushState and replaceState calls on the History API. When Next.js navigates to a new route, it calls history.pushState(), which triggers the analytics script to record a new pageview. This is seamless — you add the script tag once and it handles all navigations automatically.

Manual tracking with usePathname. For npm-based analytics or Google Analytics, you often need to manually send pageview events on route changes:

// app/analytics.tsx
'use client'

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

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

  useEffect(() => {
    // Send pageview to your analytics tool
    gtag('event', 'page_view', {
      page_path: pathname,
    })
  }, [pathname, searchParams])

  return null
}

This works but adds a Client Component to your layout, requires manual integration, and means the analytics tracking code is coupled to your application code. If you forget to add this component, client-side navigations are invisible to your analytics.

Google Analytics with Next.js

Adding GA4 to Next.js requires two scripts — the gtag.js loader and the configuration script:

// app/layout.tsx
import Script from 'next/script'

const GA_ID = 'G-XXXXXXXXXX'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
          strategy="afterInteractive"
        />
        <Script id="ga-init" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${GA_ID}');
          `}
        </Script>
      </body>
    </html>
  )
}

This adds approximately 45 KB of JavaScript to every page load. You also need the route change tracking component described above, a cookie consent banner for EU visitors, and potentially a consent mode configuration to comply with GDPR.

GA4 with Next.js is not hard to set up, but it involves more moving parts than simpler alternatives. The consent requirement alone — implementing a consent management platform, conditionally loading the script, handling consent mode — can take hours to implement correctly.

Performance impact

Analytics scripts affect your Core Web Vitals — specifically Largest Contentful Paint (LCP), First Input Delay (FID), and Total Blocking Time (TBT). The impact depends on the script size and execution behavior.

Script size comparison for Next.js:

ToolSize (gzipped)Setup
sourcebeam< 1 KBScript tag
Plausible~1.5 KBScript tag
Fathom~6 KBScript tag
Vercel Analytics~1 KBnpm package
Mixpanel~30 KBnpm package
Google Analytics 4~45 KBScript + config
PostHog~60 KBnpm package

For Next.js applications optimized for performance — especially marketing sites, landing pages, and content pages where Lighthouse scores directly impact SEO rankings — the difference between a sub-1 KB script and a 60 KB SDK is measurable. On a 3G connection, 60 KB takes roughly 500ms to download. On a slow 2G connection, it takes over 2 seconds.

Beyond download time, larger SDKs take longer to parse and execute. PostHog's autocapture scans the DOM for clickable elements. Mixpanel's persistence layer reads and writes cookies. These operations contribute to Total Blocking Time and can delay interactivity.

Server-side analytics

Next.js Server Components and Route Handlers run on the server, which opens the possibility of server-side analytics — tracking page renders without any client-side JavaScript.

In theory, you could track pageviews from a Server Component by making an API call when the component renders. In practice, this has significant limitations:

Client-side navigations are invisible. When Next.js navigates between routes on the client side, Server Components for the new route are fetched via RSC payload — but this does not trigger a traditional server render. You would only capture the initial page load, not subsequent navigations.

No client-side context. Server Components do not have access to browser APIs — screen size, referrer (from document.referrer), timezone, or language preferences from the browser. You only get what the HTTP request provides — the User-Agent header and IP address.

Caching and ISR complicate things. If a page is statically generated or cached by ISR, the Server Component does not re-render for each visitor. Your analytics would only count cache misses, not actual visitors.

For these reasons, client-side analytics (via script tag or SDK) remains the practical approach for Next.js applications. Server-side tracking can supplement client-side data — for example, logging API route usage from Route Handlers — but it cannot replace it.

The recommendation

For most Next.js applications, the best approach is a lightweight script tag in your root layout. It requires no npm packages, no build step changes, no Client Component wrappers, and no manual route change tracking. It works with the App Router and the Pages Router identically.

If performance matters to you — and in Next.js, it usually does — understanding how to speed up your website is essential, and that starts with choosing an analytics tool with a small script. A sub-1 KB script that loads asynchronously has effectively zero impact on your Core Web Vitals. A 60 KB SDK that initializes providers and scans the DOM has a measurable one.

If privacy matters to you — and to your users — choose a cookie-free analytics tool that can track visitors without cookies. It eliminates the consent banner implementation (which in Next.js means another Client Component, another provider, and conditional script loading logic), simplifies your compliance posture, and improves the visitor experience.

One script tag. Under 1 KB. No cookies. No consent banner. No npm package. That is the setup, and it takes 30 seconds.

sourcebeam works with Next.js out of the box — one script tag, sub-1 KB, automatic route change detection. Try it free