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.
| Area | Primary lever | Metric impact |
|---|---|---|
| Images | next/image, priority, sizes | LCP, CLS |
| Fonts | next/font, display: swap | LCP, CLS |
| JS bundles | dynamic import, Server Components | INP, TTFB |
| Navigation | Link prefetch, Router Cache | INP, perceived speed |
| SEO | metadata, sitemap, robots | Crawlability, ranking signals |
Core Web Vitals in Next.js
Definition
Google’s Core Web Vitals — measured at p75 field data:
| Metric | Measures | Good (p75) | Poor (p75) |
|---|---|---|---|
| LCP | Largest content paint | ≤ 2.5s | > 4.0s |
| INP | Worst interaction latency | ≤ 200ms | > 500ms |
| CLS | Unexpected 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 spaceField 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 elementInterview:
priorityon the wrong image wastes bandwidth.sizesmust 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-facewithoutdisplay: 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 configuredKeep 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 routesInterview: INP is often client JS on the main thread. Server Components are the first bundle win — not
memoon a 400 KB chart library.
Link prefetch and Router Cache
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 clearStale 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:
metadataBaseresolves relative OG image URLs.generateMetadataruns 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 mutationSecurity 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/imagefor LCP + CLS.next/fontfor self-hosted fonts.priorityandsizesmust match layout.
Bundles
Server Components default. Dynamic import for heavy client widgets. Analyze with bundle analyzer.
SEO
metadata/generateMetadata,sitemap.ts,robots.ts.metadataBasefor 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