How to Add Analytics to a Next.js App (Without Hurting Performance)
Meta Description: Install a cookieless tracker in Next.js with proper SPA route-change handling, no hydration issues, and zero Core Web Vitals impact.
Next.js is fast. Really fast. You've probably spent time optimizing for Core Web Vitals, cutting bundle size, and implementing image optimization. The last thing you need is analytics adding bloat and slowing things down.
The good news: you can add production-grade analytics to Next.js in three lines of code. No hydration mismatches, no performance penalties, no configuration hell.
This guide shows you exactly how to do it—plus how to handle SPA route changes, verify it's working, and troubleshoot if things go sideways.
Why Analytics Can Break Next.js
Next.js runs JavaScript on both the server (during SSR) and the client (during hydration). This dual nature creates edge cases that naive analytics implementations run headfirst into.
The Hydration Problem
When you include analytics code in pages/_app.tsx, it might run differently on the server vs. the client:
- Server-side: The component renders, and your analytics code runs (generating a unique ID, setting a timestamp, etc.)
- Client-side: React hydrates the page, and your analytics code runs again, generating a different ID or timestamp
Now your server-rendered HTML doesn't match what the client rendered—hydration mismatch. React throws a warning; performance suffers.
The Route-Change Problem
In a traditional SPA, every route change is visible to the analytics script. In Next.js, which uses file-based routing, route changes happen without a traditional SPA router event.
If your analytics only listens for hashchange or popstate, it'll miss most route transitions in Next.js apps. Every navigation looks like the same page to your tracker.
The Performance Problem
Some analytics scripts:
- Load synchronously, blocking page rendering
- Run heavy JavaScript on every pageview
- Generate cookies or local storage access patterns that violate Lighthouse best practices
- Cause CLS (Cumulative Layout Shift) by injecting DOM elements
The solution is to choose analytics built for modern web performance.
Copy-Paste Install in 3 Lines
Here's the fastest way to add Statalog (or any lightweight analytics) to Next.js:
Option 1: Using next/script (Recommended)
// pages/_app.tsx (or app/layout.tsx for App Router)
import Script from 'next/script';
export default function App({ Component, pageProps }) {
return (
<>
<Script
async
src="https://cdn.statalog.com/st.js"
data-site="ST-XXXXXXX"
/>
<Component {...pageProps} />
</>
);
}
What's happening:
next/scriptloads the script async, so it doesn't block page renderingdata-site="ST-XXXXXXX"tells Statalog which site to track (replace with your actual site ID from your Statalog dashboard)- The script automatically detects Next.js route changes and tracks pageviews
That's it. No hydration issues, no configuration, automatic route tracking.
Option 2: Plain Script Tag
If you prefer a regular HTML script tag:
<!-- pages/_document.tsx -->
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<script
async
src="https://cdn.statalog.com/st.js"
data-site="ST-XXXXXXX"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Both approaches work. The next/script approach is cleaner and more consistent with Next.js best practices.
Handling Route Changes in Next.js
The Statalog script automatically detects Next.js route changes. You don't need to do anything special.
However, if you want to track specific route transitions or add custom logic, you can use the Next.js useRouter hook:
// pages/blog/[slug].tsx
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function BlogPost() {
const router = useRouter();
useEffect(() => {
// This runs every time the route changes
console.log('Navigated to:', router.asPath);
}, [router.asPath]);
return (
<article>
<h1>{/* your content */}</h1>
</article>
);
}
For App Router (Next.js 13+):
// app/blog/[slug]/page.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export default function BlogPost() {
const pathname = usePathname();
useEffect(() => {
console.log('Navigated to:', pathname);
}, [pathname]);
return (
<article>
<h1>{/* your content */}</h1>
</article>
);
}
The analytics script listens for these route changes automatically—you're just adding custom logging here if you need it.
No Impact on Core Web Vitals
The Statalog script is built specifically to avoid Web Vitals penalties:
2KB Gzipped
The entire script is < 2KB. It doesn't bloat your JavaScript bundle.
Async Loading
Using <Script async> means the script loads in the background. Your First Contentful Paint (FCP) and Largest Contentful Paint (LCP) aren't affected.
No DOM Manipulation
The script doesn't inject elements into the DOM, so no Cumulative Layout Shift (CLS).
First-Party, No Cookies
Statalog is cookieless and first-party by default. No third-party tracking cookies = better privacy, better Lighthouse scores, no GDPR headaches.
Result
Your Core Web Vitals stay green. You get analytics without the performance tax.
Optional: Track Custom Events
Pageview tracking is automatic. But you might want to track specific user interactions—button clicks, form submissions, video plays, etc.
The Statalog script exposes a global function for custom events:
// Track a button click
document.getElementById('signup-button').addEventListener('click', () => {
window.statalog?.('event', 'Signup Button Clicked', {
button_location: 'hero',
experiment: 'variant_b'
});
});
Or for form submissions:
// Track form submission
const form = document.getElementById('contact-form');
form?.addEventListener('submit', (e) => {
window.statalog?.('event', 'Contact Form Submitted', {
email_domain: new URL(e.target.email.value).hostname
});
// then submit the form normally
});
The ?. operator is defensive—it only calls the function if window.statalog exists. This prevents errors if the script hasn't loaded yet.
Testing It Works
After adding the script, verify it's tracking:
Check DevTools Network Tab
- Open DevTools (F12)
- Go to the Network tab
- Navigate your Next.js app to a new page
- Look for requests to
api.statalog.comorcdn.statalog.com - You should see a request with your site ID in the query string
Check the Statalog Dashboard
- Log into your Statalog account
- Go to your site's dashboard
- Check the Live or Real-time report
- You should see your own pageviews appearing in real-time
Check Browser Console
The analytics script might log debug info:
// In DevTools console, after the script loads:
console.log(window.statalog); // Should be a function, not undefined
If you see undefined, the script didn't load. Check the Network tab to see if the CDN URL is correct.
Troubleshooting
Problem: Hydration Mismatch Error
Error: "Text content does not match server-rendered HTML"
Cause: Your analytics code ran differently on server vs. client.
Solution:
- Use
next/scriptwith the async attribute (shown above) - Don't run analytics code in the component body; only in
useEffector_app.tsx - Check that
data-siteattribute is set correctly
Problem: No Pageviews Recorded
Check:
- Is the script loading? Check Network tab in DevTools
- Is your site ID correct? Check Statalog dashboard for the exact ID
- Are you on
localhost? Local traffic is usually filtered; check dashboard settings - Is the analytics dashboard showing other sites' data? Wrong site ID
Solution:
- Copy the exact site ID from your Statalog dashboard
- Use
next/scriptinstead of a manual script tag - Check dashboard filters (Local/Test traffic toggle)
Problem: Routes Not Tracked in SPA Mode
Cause: Next.js App Router or dynamic routes aren't triggering analytics.
Solution:
- Verify the script is async and loading from the correct CDN
- Check that route changes are updating the URL (check address bar)
- Use the manual
useRouter()orusePathname()approach above to verify routing works
Problem: Performance Degradation
Check:
- Is the script blocking page load? Check Network waterfall in DevTools
- Is the script synchronous? It should have
asyncattribute
Solution:
- Always use
<Script async>or addasyncattribute - Don't load analytics synchronously in
_document.tsx
Code Example: Full Integration
Here's a complete example of analytics in a Next.js App Router project:
// app/layout.tsx
import type { Metadata } from 'next';
import Script from 'next/script';
export const metadata: Metadata = {
title: 'My Next.js App',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<Script
async
src="https://cdn.statalog.com/st.js"
data-site="ST-XXXXXXX"
/>
</head>
<body>{children}</body>
</html>
);
}
For Pages Router:
// pages/_app.tsx
import type { AppProps } from 'next/app';
import Script from 'next/script';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script
async
src="https://cdn.statalog.com/st.js"
data-site="ST-XXXXXXX"
/>
<Component {...pageProps} />
</>
);
}
Both are production-ready. Pick the one matching your Next.js version.
Frequently Asked Questions
Does it slow down my app? No. The 2KB script loads asynchronously and runs in the background. No performance penalty.
Is it safe for SSR? Yes. The script handles both server-rendered and hydrated content correctly. No hydration mismatches.
Does it work with middleware? Yes. The script works independently of Next.js middleware. Middleware doesn't affect analytics.
Can I use environment variables for the site ID?
Yes. Replace ST-XXXXXXX with your environment variable:
<Script
async
src="https://cdn.statalog.com/st.js"
data-site={process.env.NEXT_PUBLIC_STATALOG_SITE_ID}
/>
(Use NEXT_PUBLIC_ prefix so it's available in the browser.)
What about API routes? API routes don't generate pageviews. Analytics tracks client-side navigation. If you want to track API usage, use custom events or server-side logging.
Can I disable analytics in development? Yes. Only initialize the script in production:
{process.env.NODE_ENV === 'production' && (
<Script
async
src="https://cdn.statalog.com/st.js"
data-site="ST-XXXXXXX"
/>
)}
That's it. Three lines of code, no configuration, automatic route tracking, and zero performance impact. Your Next.js app is now production analytics-ready.
Ready to add analytics? Start with the copy-paste code above, verify it's working with DevTools, and check your dashboard for real-time data.
Need more? Check out the full Statalog documentation or performance best practices for Next.js.