EmpireUI
Get Pro
← Blog8 min read#analytics#next.js#vercel

Analytics in Next.js: Vercel Analytics, PostHog, Plausible Setup

Set up Vercel Analytics, PostHog, and Plausible in Next.js 14+ the right way — no bloat, no cookie banners where you don't need them, real data.

Developer screen showing Next.js analytics dashboard code and charts

Why Analytics in Next.js Is Weirdly Complicated

You'd think dropping analytics into a Next.js app would be a five-minute job. It used to be — paste a <script> tag, move on. But with the App Router, Server Components, and Partial Prerendering landing in Next.js 14+, where you put that code actually matters a lot.

The core issue is that analytics scripts are client-side by default, but a bunch of your app might be server-rendered or even static. If you try to call window.analytics.page() inside a Server Component, you're going to get a runtime error on the first deploy and a very confusing morning.

Honestly, the landscape is also just fragmented. Vercel Analytics, PostHog, and Plausible all solve slightly different problems — and picking the wrong one (or running all three simultaneously on a lightweight landing page) has a real performance cost. A poorly-loaded analytics bundle added to a 2024 benchmark showed 18–40ms of added LCP in some cases. That's not nothing.

This guide covers all three tools, where to mount them in an App Router project, what the actual tradeoffs are, and how to avoid the footguns that bite people every week on Stack Overflow.

Vercel Analytics: The Zero-Config Option

If your app is already deployed on Vercel, this one's almost free. Vercel Analytics is baked into the platform and gives you page views, unique visitors, and Web Vitals (LCP, CLS, FID, TTFB) without any custom event tracking setup. You just install the package and drop two components.

npm install @vercel/analytics @vercel/speed-insights

In your root layout.tsx, import from the package — they're already client-side wrapped, so you don't need 'use client' on the layout itself: ``tsx // app/layout.tsx import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/next'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Analytics /> <SpeedInsights /> </body> </html> ); } ``

That's it. Enable the feature in your Vercel dashboard under the Analytics tab and data starts flowing. Worth noting: it's privacy-friendly by default — no cookies, IP addresses are never stored, GDPR-friendly out of the box. For a lot of projects that's enough to skip the cookie consent banner entirely.

The downside? It's Vercel-only and event tracking is very limited without upgrading. You can't do funnels, cohort analysis, or session replay on the free tier. If you need those, keep reading.

PostHog: Full Product Analytics Without the SaaS Lock-In

PostHog is what you reach for when you need actual product analytics — funnels, feature flags, session recordings, A/B testing, event pipelines, all from one tool you can self-host if you want. It's genuinely impressive. In 2025 they shipped a JS SDK that tree-shakes properly, so the base bundle is around 30kb gzipped when you're not pulling in the replay module.

Install the Next.js provider: ``bash npm install posthog-js ``

Because PostHog's posthog-js needs access to window, you need a proper client component provider. Don't try to initialize it in a Server Component — you'll have a bad time. ``tsx // providers/PostHogProvider.tsx 'use client'; import posthog from 'posthog-js'; import { PostHogProvider as PHProvider } from 'posthog-js/react'; import { useEffect } from 'react'; export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com', capture_pageview: false, // We handle this manually persistence: 'memory', // No cookies — switch to 'localStorage' if you want sessions }); }, []); return <PHProvider client={posthog}>{children}</PHProvider>; } ``

Notice capture_pageview: false. With the App Router doing client-side navigation, PostHog's auto-pageview fires once on load and then misses soft navigations. You need to manually track route changes. Wire up a usePathname watcher in that same provider: ``tsx import { usePathname, useSearchParams } from 'next/navigation'; // Inside PostHogProvider, after posthog.init: const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { if (pathname) { posthog.capture('$pageview', { $current_url: window.location.href, }); } }, [pathname, searchParams]); ``

One more thing — if you're running PostHog Cloud and you're concerned about ad blockers, PostHog supports reverse-proxying through your own domain via Next.js rewrites. Add this to next.config.ts: ``ts awaitasync rewrites() { return [ { source: '/ingest/:path*', destination: 'https://app.posthog.com/:path*', }, ]; } ` Then set api_host to /ingest`. Suddenly your events stop getting dropped by Brave.

Plausible: Privacy-First, Lightweight, Honest

Plausible is 1kb. Not 1kb gzipped — 1kb flat. If you're building a content site, a docs page, or anything where raw performance matters more than session replay and funnel analysis, Plausible is the right call. It's cookie-free, GDPR/CCPA compliant without any configuration, and the dashboard is genuinely beautiful.

The setup in Next.js is straightforward. Unlike PostHog, Plausible's script is just a <script> tag — but you should load it correctly with Next.js's <Script> component so it doesn't block rendering: ``tsx // app/layout.tsx import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js" strategy="afterInteractive" /> </body> </html> ); } ``

For custom events, Plausible exposes a plausible() global that you can call from anywhere client-side. In TypeScript you'll want to extend the window type: ``ts // types/plausible.d.ts declare global { interface Window { plausible?: (event: string, options?: { props?: Record<string, string | number> }) => void; } } ` Then track events like this: `ts window.plausible?.('SignUp', { props: { plan: 'pro' } }); ``

Quick aside: Plausible also supports self-hosting via Docker if you're on a tight budget or need data residency in the EU. The community edition is fully open source. That's a pretty different value proposition from Vercel Analytics which is fully locked to the Vercel platform.

In practice, Plausible and Vercel Analytics cover 80% of what most teams actually look at: page views, referrers, top pages, browsers, devices. If you catch yourself checking PostHog's session replays more than once a week, you need PostHog. Otherwise, you probably don't.

Running Multiple Providers Without Blowing Up Performance

Here's a pattern you'll see on real production apps: Vercel Analytics for Web Vitals monitoring (because it's already there and free), Plausible for traffic metrics (because it's 1kb and the team trusts it), and PostHog specifically on authenticated pages for product event tracking. This is actually a reasonable split — as long as you're not loading PostHog's full bundle on every public page.

The trick is conditional initialization. In Next.js App Router you can pass a user prop down from a Server Component to decide whether to init PostHog: ``tsx // app/layout.tsx (Server Component) import { getSession } from '@/lib/auth'; import { AnalyticsProviders } from '@/providers/AnalyticsProviders'; export default async function RootLayout({ children }) { const session = await getSession(); return ( <html lang="en"> <body> <AnalyticsProviders enablePostHog={!!session}> {children} </AnalyticsProviders> </body> </html> ); } ` Then inside AnalyticsProviders (a Client Component), only mount the PostHog provider when enablePostHog` is true. Public visitors get Plausible's 1kb script. Logged-in users get the full PostHog suite.

Look, you can also just run all three everywhere and call it a day — but you're then loading ~80kb of analytics JS on every page, and that genuinely affects your Core Web Vitals score. If you're using a UI library that already does some work on load, stacking analytics carelessly makes it worse. Empire UI's components are optimized for LCP-sensitive layouts, and that effort gets undermined if your analytics stack is lazy about loading.

One thing worth testing: use Next.js's strategy="lazyOnload" on your Plausible or PostHog script if it's loaded via <Script>. It defers loading until everything else is done — at the cost of potentially missing very fast bounces. Whether that tradeoff is worth it depends on your traffic patterns.

Environment Variables and the Local Dev Trap

You don't want analytics firing in development. It pollutes your data and makes debugging harder. All three tools handle this differently, and you need to set it up explicitly or you'll spend an afternoon wondering why your localhost traffic shows up in your dashboard.

For Vercel Analytics, it's automatic — the SDK reads VERCEL_ENV and only fires on deployments. You don't have to do anything. For PostHog and Plausible, you have to guard it yourself: ``ts // PostHog — only init in production posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, loaded: (ph) => { if (process.env.NODE_ENV === 'development') ph.opt_out_capturing(); }, }); ` For Plausible, add data-exclude="/" in development or simply skip rendering the Script component: `tsx {process.env.NODE_ENV === 'production' && ( <Script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js" strategy="afterInteractive" /> )} ``

Also set up your .env.local properly. Keep keys out of your repo — this is obvious, but I still see PostHog keys hardcoded in GitHub repos weekly: `` # .env.local NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com ` Prefix with NEXT_PUBLIC_` only because these need to be available on the client. Server-only secrets should never get that prefix.

If you want a good companion read on how Next.js handles metadata and SEO signals alongside your analytics setup, nextjs-metadata-seo covers the full picture. It pairs well with this guide once you've got events flowing.

Validating Your Setup Actually Works

Don't trust your setup until you've manually verified events are hitting the right endpoint. Each tool has a debug mode and you should use it before shipping.

For PostHog, enable the toolbar by calling posthog.debug() in the browser console. You'll get a floating overlay showing captured events in real time. For Plausible, open DevTools Network tab and filter by plausible.io — you should see a 202 response to https://plausible.io/api/event on each page transition. For Vercel Analytics, check the Vercel dashboard's real-time view after deploying to a preview branch.

There's also a useful pattern for smoke-testing your analytics in CI: mock the PostHog client during Playwright tests so events don't fire during E2E runs. Add this to your Playwright config: ``ts await page.route('**/ingest/**', (route) => route.abort()); `` Blocks the proxied PostHog endpoint entirely in tests. Clean data, clean tests.

Once everything's confirmed working, this pairs really well with caching strategies in nextjs-caching-strategies — because if you're aggressively caching pages with stale-while-revalidate, you need to understand which analytics hits come from cached responses vs. fresh ones. It affects your unique visitor counts more than you'd expect.

FAQ

Can I use PostHog and Plausible at the same time?

Yes. They're fully independent. A common pattern is Plausible for traffic metrics on public pages and PostHog on authenticated user flows for product analytics — you only load PostHog where you need it.

Does Vercel Analytics work outside of Vercel deployments?

No. It's tied to Vercel's infrastructure and requires your app to be deployed there. If you're on Railway, Fly.io, or a VPS, use Plausible or PostHog instead.

Do I need a cookie consent banner for Plausible?

No. Plausible is cookie-free and doesn't track personal data, so it's compliant with GDPR, CCPA, and PECR without a consent banner — as long as you're using Plausible's standard script.

Why are my PostHog pageviews duplicated in Next.js App Router?

You have both auto-pageview enabled and a manual $pageview capture wired up. Set capture_pageview: false in posthog.init() and handle page tracking yourself with a usePathname effect.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Next.js OG Image Generation: @vercel/og, Edge Runtime, Custom FontsDeploying Next.js in 2026: Vercel, Docker, VPS ComparedEdge Runtime in Next.js: Middleware, Edge API Routes and LimitsVercel Edge Config: Feature Flags, A/B Tests Without Redeploy