Installing GA4 on Next.js has a few specifics compared to a classic site: hybrid rendering (SSR/SSG), client-side navigation without page reload (SPA), and performance best practices. Here's the recommended method for each configuration.
Method 1: @next/third-parties (recommended: App Router)
Since Next.js 14, the official @next/third-parties package offers an optimised GoogleAnalytics component: deferred loading, no render blocking, compatible with Server Components.
Install
npm install @next/third-parties
Integrate in the root layout
// src/app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</body>
</html>
)
}
Replace G-XXXXXXXXXX with your GA4 Measurement ID.
Pros:
- Optimised loading via automatic
Strategy.afterInteractive - Respects Core Web Vitals best practices (no blocking script)
- Official Next.js, maintained by Vercel
- Compatible with Server Components
Method 2: manual gtag.js with the Script component
If you don't want an extra dependency or if you need precise control over events:
// src/app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<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>
</head>
<body>{children}</body>
</html>
)
}
Important: always use strategy="afterInteractive" and not strategy="beforeInteractive". The GA4 script is not critical for the initial render and shouldn't block page display.
Method 3: Pages Router (Next.js 12 and earlier)
For projects on the old Pages Router, the tag is added in _app.tsx or _document.tsx:
// pages/_app.tsx
import Script from 'next/script'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
strategy="afterInteractive"
/>
<Script id="ga4-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
<Component {...pageProps} />
</>
)
}
Handling GDPR consent
If your site is subject to GDPR (which is the case for any EU-facing site), the GA4 tag should only load after user consent.
Conditional consent approach
// src/app/layout.tsx
'use client'
import { GoogleAnalytics } from '@next/third-parties/google'
import { useConsent } from '@/hooks/useConsent' // your consent hook
export function AnalyticsProvider() {
const { analyticsConsented } = useConsent()
if (!analyticsConsented) return null
return <GoogleAnalytics gaId="G-XXXXXXXXXX" />
}
With GA4 Consent Mode
GA4 supports Consent Mode v2: you can load the tag without data before consent, and enable full tracking after agreement.
<Script id="ga4-consent-init" strategy="beforeInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied'
});
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
After user consent, update the status:
gtag('consent', 'update', {
analytics_storage: 'granted'
})
Tracking page changes (SPA)
Next.js is a Single Page Application: navigations between pages don't reload the document, so GA4 doesn't automatically detect page changes.
Good news: since GA4, page-view tracking in SPAs is handled automatically via the History API. You normally don't need extra code to track Next.js navigations.
If you see duplicates or missing pages in GA4, check you don't have two active GA4 tag instances (one via @next/third-parties and another via GTM or another script).
Sending custom events
// Example: track a CTA click
'use client'
function CTAButton() {
const handleClick = () => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'cta_click', {
event_category: 'engagement',
event_label: 'hero_button',
})
}
}
return <button onClick={handleClick}>Get started</button>
}
Declare the gtag type in your TypeScript declaration file if needed:
// src/types/gtag.d.ts
declare function gtag(...args: unknown[]): void
declare interface Window {
gtag: typeof gtag
dataLayer: unknown[]
}
Verification
- Run
npm run dev - Open
http://localhost:3000in a private window - In GA4 → Reports → Realtime: you should appear as a visitor
If data doesn't come in locally, it's often due to:
- An ad blocker active in the browser
- The tag loaded only in production (env variable
NODE_ENV) - Consent Mode set to "denied" by default
Related articles
- Complete GA4 install guide: the all-platform overview
- Google Tag Manager with GA4: if you prefer GTM
- GA4 and GDPR cookies consent: compliance setup
If you manage GA4 for multiple Next.js projects, NarratIQ centralises their properties in one dashboard and generates the monthly PDF report without manual extraction.
Install guides on other platforms
- GA4 install: general guide: the full procedure across all platforms, property creation included
- Install GA4 on WordPress: for WordPress and WooCommerce sites
- Install GA4 on Shopify: for Shopify e-commerce stores