Third-party code can delay rendering, occupy the main thread, and shift layout. Treat every analytics tag, widget, and embed as a performance cost that must earn its place.
Outcome
Google Analytics loaded with Next.js's maintained integration, optional scripts deferred with an appropriate strategy, and a before/after audit based on measured lab and field data.
Fast Track
- Use
@next/third-parties/googlefor Google products. - Use
next/scriptfor other providers and choose the least eager strategy that still meets the feature requirement. - Compare Lighthouse Total Blocking Time (TBT) before and after the change; use real-user Interaction to Next Paint (INP) to judge production responsiveness.
Prefer Maintained Integrations
Install Next.js's third-party component package:
pnpm add @next/third-parties@latestThen add Google Analytics to the root layout. The component loads the Google script after hydration and supports client-side navigation tracking when Enhanced Measurement is configured in Google Analytics.
import { GoogleAnalytics } from '@next/third-parties/google'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID!} />
</html>
)
}Only expose the measurement ID through NEXT_PUBLIC_GA_ID; it is an identifier, not a secret. Keep API secrets and credentials in server-only environment variables.
To send an event from a Client Component:
'use client'
import { sendGAEvent } from '@next/third-parties/google'
export function SignupButton() {
return (
<button
type="button"
onClick={() => sendGAEvent('event', 'signup_started', { source: 'header' })}
>
Start free
</button>
)
}Loading Other Providers with next/script
Use next/script when Next.js does not provide a maintained component for the provider.
| Strategy | When it loads | Appropriate use |
|---|---|---|
beforeInteractive | Before Next.js hydration | A rare, site-wide script whose documented requirement is to run before hydration |
afterInteractive | After some hydration | Providers needed soon after the page becomes interactive |
lazyOnload | During browser idle time | Chat, social widgets, and other optional features |
worker | In a worker | Experimental, Pages Router-only cases after compatibility testing |
Most optional widgets should wait until idle:
import Script from 'next/script'
export default function SupportPage() {
return (
<>
<main>
<h1>Support</h1>
</main>
<Script
src="https://widget.example.com/chat.js"
strategy="lazyOnload"
/>
</>
)
}Do not classify a script as critical from its product category alone. Confirm the provider's integration requirements, its need for DOM access, and whether the user needs it during the initial interaction.
Measure Instead of Estimating
Lighthouse reports TBT as a lab proxy for main-thread responsiveness. INP is a field Core Web Vital based on real user interactions. They answer related but different questions:
- Use TBT for repeatable local comparisons while changing script loading.
- Use INP from production field data to determine whether real interactions are responsive.
- Also inspect the Network and Performance panels for transfer size, long tasks, request chains, and layout shifts.
Keep the test conditions identical. Run multiple samples because a single Lighthouse result is noisy.
| Run | Analytics | Optional widget | TBT | Notes |
|---|---|---|---|---|
| Baseline | Current integration | Current integration | Record result | Same device and throttling |
| Isolation | Blocked in DevTools | Blocked in DevTools | Record result | Estimates total third-party cost |
| Candidate | Next.js integration | lazyOnload | Record result | Compare with the baseline |
Do not write a fixed savings claim before collecting these results. A provider's cost varies with its configuration, network, device, consent state, and the other work on the page.
Hands-On Exercise 4.3
Audit the scripts in apps/web/src/app/layout.tsx and replace the hand-rolled Google Analytics tags.
Requirements:
- Install
@next/third-parties. - Remove the raw Google Analytics
<script>tags and inlinegtagbootstrap. - Add
<GoogleAnalytics gaId={...} />to the root layout. - Classify every remaining third-party script by user need and loading requirement.
- Capture at least three Lighthouse runs before and after the change and compare the median TBT.
- Record where production INP will be monitored after deployment.
Try It
- Open Chrome DevTools and record a Performance trace during page load.
- Identify long tasks attributed to third-party origins.
- Run Lighthouse at least three times under the same throttling profile and record the median TBT.
- Block one third-party origin in DevTools, repeat the test, and use the difference as evidence for keeping, removing, or deferring that provider.
- Confirm analytics events arrive without double-counting page views.
Done-When
- The layout uses
GoogleAnalyticsfrom@next/third-parties/google; no hand-rolled GA bootstrap remains. - No page loads a mutable remote polyfill service.
- Each remaining external script has a documented owner, purpose, and loading strategy.
- The before/after table contains measured median TBT from at least three comparable runs.
- The deployment plan names the field-data source used to monitor INP.
- Analytics events and client-side page views are verified without duplicates.
Solution
Google Analytics integration
import type { Metadata } from 'next'
import { GoogleAnalytics } from '@next/third-parties/google'
import './globals.css'
export const metadata: Metadata = {
title: process.env.NEXT_PUBLIC_APP_NAME ?? 'Vercel Academy Foundation - Web',
description: 'VAF Web',
}
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body className="container mx-auto px-4 py-8">{children}</body>
{process.env.NEXT_PUBLIC_GA_ID ? (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
) : null}
</html>
)
}This solution delegates Google Analytics loading to the maintained Next.js component. It does not need a Client Component solely for an onLoad callback; validate the integration in the browser's Network panel and in Google Analytics DebugView instead.
References
Was this helpful?