Topics
v5.0.0
On this page · sections

Performance, SEO & Production

Production Next.js apps optimize Core Web Vitals, asset delivery, and discoverability through framework primitives — next/image, next/font, metadata APIs, and bundle-aware routing. Lab scores do not ship; field data at p75 drives Search signals and user retention.

AreaPrimary leverMetric impact
Imagesnext/image, priority, sizesLCP, CLS
Fontsnext/font, display: swapLCP, CLS
JS bundlesdynamic import, Server ComponentsINP, TTFB
NavigationLink prefetch, Router CacheINP, perceived speed
SEOmetadata, sitemap, robotsCrawlability, ranking signals

Core Web Vitals in Next.js

Definition

Google’s Core Web Vitals — measured at p75 field data:

MetricMeasuresGood (p75)Poor (p75)
LCPLargest content paint≤ 2.5s> 4.0s
INPWorst interaction latency≤ 200ms> 500ms
CLSUnexpected layout shift≤ 0.1> 0.25

Next.js levers:

LCP → prioritize hero image, static shell/PPR, edge caching, font preload INP → reduce client JS, Server Components, code splitting, avoid long tasks CLS → next/image dimensions, font swap, reserve skeleton space

Field monitoring in production:

// app/components/WebVitals.tsx "use client"; import { useReportWebVitals } from "next/web-vitals"; export function WebVitals() { useReportWebVitals((metric) => { const body = JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, rating: metric.rating, path: window.location.pathname, }); navigator.sendBeacon("/api/vitals", body); }); return null; }

Interview: p75 because median hides slow tail on low-end devices. INP replaced FID in March 2024. CrUX lags ~28 days — ship your own RUM.

next/image

Definition

next/image optimizes format (WebP/AVIF), sizing, lazy loading, and placeholder blur — reducing LCP bytes and preventing layout shift when width/height or fill + parent sizing are set.

import Image from "next/image"; export function Hero() { return ( <Image src="/hero.jpg" alt="Product showcase" width={1200} height={630} priority // LCP candidate — preload, no lazy load sizes="(max-width: 768px) 100vw, 1200px" placeholder="blur" blurDataURL="data:image/jpeg;base64,..." /> ); }

Remote images require remotePatterns in next.config.ts:

const nextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "cdn.example.com", pathname: "/assets/**" }, ], }, }; export default nextConfig;

Rules:

LCP image → priority + correct sizes Below fold → default lazy load Always → explicit dimensions or fill + sized parent Avoid → CSS-only background-image for LCP element

Interview: priority on the wrong image wastes bandwidth. sizes must match real layout breakpoints.

next/font

Definition

next/font self-hosts fonts at build time — no external Google Fonts request, no layout shift from invisible text if paired with display: swap.

// app/layout.tsx import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter", }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={inter.variable}> <body className="font-sans">{children}</body> </html> ); }

Local font:

import localFont from "next/font/local"; const brand = localFont({ src: "../public/fonts/Brand.woff2", display: "swap", variable: "--font-brand", });

Interview: Self-hosted fonts cut third-party latency and improve CLS vs unstyled @font-face without display: swap.

Bundle size and code splitting

Definition

Server Components ship zero client JS for their logic. Client boundaries ('use client') and dynamic imports control what hydrates.

// app/dashboard/page.tsx — heavy chart only loads on dashboard import dynamic from "next/dynamic"; const RevenueChart = dynamic(() => import("./RevenueChart"), { ssr: false, loading: () => <div className="skeleton h-64" />, }); export default function DashboardPage() { return ( <main> <h1>Dashboard</h1> <RevenueChart /> </main> ); }

Analyze bundles:

ANALYZE=true npm run build # with @next/bundle-analyzer configured
Keep server by default → 'use client' only for interactivity Dynamic import heavy libs → charts, editors, maps Barrel files → can pull entire libraries — import direct paths Shared chunks → inspect duplicate deps across routes

Interview: INP is often client JS on the main thread. Server Components are the first bundle win — not memo on a 400 KB chart library.

Definition

Link prefetches RSC payloads for visible routes in production — soft navigation feels instant when the Router Cache holds a fresh payload.

import Link from "next/link"; export function Nav() { return ( <nav> <Link href="/dashboard" prefetch={true}>Dashboard</Link> <Link href="/settings" prefetch={false}>Settings</Link> </nav> ); }
prefetch={true} → prefetch when link enters viewport (production) prefetch={false} → skip — heavy or auth-gated routes router.refresh() → bust Router Cache for current route Hard reload → full cache clear

Stale soft navigation is a common bug — pair mutations with revalidatePath so the next navigation fetches fresh data.

Interview: Prefetch loads the RSC flight payload, not a full HTML document. Disable on rarely visited or personalized routes.

Metadata API

Definition

Export metadata or generateMetadata from layouts and pages for title, description, Open Graph, and canonical URLs.

// app/blog/[slug]/page.tsx import type { Metadata } from "next"; type Props = { params: Promise<{ slug: string }> }; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await getPost(slug); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [{ url: post.coverUrl, width: 1200, height: 630 }], type: "article", }, alternates: { canonical: `https://example.com/blog/${slug}` }, }; }

Static defaults in root layout:

// app/layout.tsx export const metadata: Metadata = { metadataBase: new URL("https://example.com"), title: { default: "Example", template: "%s | Example" }, robots: { index: true, follow: true }, };

Interview: metadataBase resolves relative OG image URLs. generateMetadata runs on the server — safe for DB-backed SEO fields.

sitemap.ts and robots.ts

Definition

Dynamic sitemap and robots files live in app/ as special Route Handlers.

// app/sitemap.ts import type { MetadataRoute } from "next"; export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } }); return [ { url: "https://example.com", lastModified: new Date(), changeFrequency: "daily", priority: 1 }, ...posts.map((p) => ({ url: `https://example.com/blog/${p.slug}`, lastModified: p.updatedAt, changeFrequency: "weekly" as const, priority: 0.8, })), ]; }
// app/robots.ts import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { return { rules: [ { userAgent: "*", allow: "/", disallow: ["/admin/", "/api/"] }, ], sitemap: "https://example.com/sitemap.xml", }; }

Interview: Sitemap generation runs at request/build depending on caching — tag and revalidate when content publishes.

Production deployment checklist

Definition

Build → npm run build; inspect bundle analyzer output Caching → CDN + Full Route Cache; verify ISR/PPR flags per route Env → server-only secrets in process.env, never NEXT_PUBLIC_ for secrets Observability → Web Vitals RUM, server traces, error tracking (Sentry) Headers → security headers in next.config or middleware Smoke test → cold start, auth flows, soft navigation after mutation

Security headers example:

const nextConfig = { async headers() { return [ { source: "/(.*)", headers: [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, ], }, ]; }, };

Senior Interview Summary

Core Web Vitals

LCP (paint), INP (interaction), CLS (layout shift). Optimize with images, fonts, and less client JS. Measure field p75.

Assets

next/image for LCP + CLS. next/font for self-hosted fonts. priority and sizes must match layout.

Bundles

Server Components default. Dynamic import for heavy client widgets. Analyze with bundle analyzer.

SEO

metadata / generateMetadata, sitemap.ts, robots.ts. metadataBase for absolute OG URLs.

Best mental model

LCP → "Hero image + shell at edge + fonts without FOIT." INP → "Less main-thread JS; Server Components first." CLS → "Dimensions on images; skeletons reserve space." Prefetch → "RSC payload ahead of click — disable on heavy routes."

Practical rule

Hero image → next/image + priority + sizes Interactive chart → dynamic(() => import(...), { ssr: false }) Blog SEO → generateMetadata + sitemap entry on publish After mutation → revalidatePath so prefetch stays fresh Production → RUM vitals + bundle analysis every release

Last updated on