Google Analytics 4 (GA4) provides traffic and user behavior data. Setting it up in a Next.js App Router application requires care to avoid performance impact and privacy compliance issues.
Step 1: Get Your Measurement ID
- Go to Google Analytics (analytics.google.com)
- Create a new property or select an existing one
- Go to Admin > Data Streams > Web
- Copy your Measurement ID (starts with
G-)
Step 2: Add the Script
Option A: Using @next/third-parties (Recommended)
pnpm add @next/third-parties
// app/layout.tsx
import { GoogleAnalytics } from "@next/third-parties/google";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</html>
);
}
This is the simplest approach. The @next/third-parties package loads GA4 with optimal performance settings.
Option B: Using next/script
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
</html>
);
}
strategy="afterInteractive" loads the script after the page becomes interactive, avoiding impact on LCP and FCP.
Step 3: Track Page Views in App Router
GA4 with gtag.js automatically tracks page views for traditional multi-page navigations. For client-side navigations in Next.js App Router, you need additional tracking:
// components/analytics/AnalyticsProvider.tsx
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
declare global {
interface Window {
gtag: (...args: unknown[]) => void;
}
}
export function AnalyticsProvider() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (typeof window.gtag !== "undefined") {
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : "");
window.gtag("config", "G-XXXXXXXXXX", {
page_path: url,
});
}
}, [pathname, searchParams]);
return null;
}
Add this to your layout:
import { Suspense } from "react";
import { AnalyticsProvider } from "@/components/analytics/AnalyticsProvider";
// In your layout body:
<Suspense fallback={null}>
<AnalyticsProvider />
</Suspense>
The Suspense boundary is required because useSearchParams needs it in Next.js App Router.
Step 4: Custom Event Tracking
Track specific user interactions:
// lib/analytics.ts
export function trackEvent(eventName: string, parameters?: Record<string, string | number>) {
if (typeof window !== "undefined" && typeof window.gtag !== "undefined") {
window.gtag("event", eventName, parameters);
}
}
Usage:
import { trackEvent } from "@/lib/analytics";
// Track a button click
<button onClick={() => {
trackEvent("cta_click", { location: "hero", variant: "primary" });
}}>
Get Started
</button>
// Track a form submission
function handleSubmit() {
trackEvent("form_submit", { form_name: "contact" });
}
// Track a download
<a
href="/whitepaper.pdf"
onClick={() => trackEvent("file_download", { file_name: "whitepaper.pdf" })}
>
Download Whitepaper
</a>
Step 5: Consent Management
For GDPR/CCPA compliance, do not load GA4 until the user consents:
// components/analytics/ConsentBanner.tsx
"use client";
import { useState, useEffect } from "react";
export function ConsentBanner() {
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
const consent = localStorage.getItem("analytics-consent");
if (consent === null) {
setShowBanner(true);
}
}, []);
function handleAccept() {
localStorage.setItem("analytics-consent", "granted");
setShowBanner(false);
// Initialize GA4
window.gtag("consent", "update", {
analytics_storage: "granted",
});
}
function handleDecline() {
localStorage.setItem("analytics-consent", "denied");
setShowBanner(false);
}
if (!showBanner) return null;
return (
<div className="fixed bottom-0 left-0 right-0 bg-white p-4 shadow-lg dark:bg-gray-900">
<p>We use cookies to analyze site traffic. Accept to help us improve.</p>
<div className="mt-2 flex gap-2">
<button onClick={handleAccept} className="rounded bg-blue-600 px-4 py-2 text-white">
Accept
</button>
<button onClick={handleDecline} className="rounded border px-4 py-2">
Decline
</button>
</div>
</div>
);
}
Set default consent to denied:
<Script id="consent-defaults" strategy="beforeInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
analytics_storage: 'denied',
});
`}
</Script>
Environment Variables
Store your measurement ID in environment variables:
# .env.local
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!} />
Only load in production:
{process.env.NODE_ENV === "production" && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!} />
)}
Verification
- Open your site in Chrome
- Open DevTools > Network tab
- Filter for "google" or "gtag"
- Verify the script loads
- Navigate between pages and check that page_view events fire
- Go to GA4 Realtime report to see live data
Need Analytics Help?
We set up analytics, track conversions, and build custom dashboards for our clients. Contact us for analytics setup and optimization.