Performance, Testing & Deployment
Ship with confidence: Core Web Vitals and how Next.js helps each one, measuring with Lighthouse and useReportWebVitals, bundle analysis with next experimental-analyze, optimizePackageImports and the React Compiler, after() and instrumentation, testing with Vitest and Playwright, and deploying to Vercel, a Node.js server, Docker (standalone output) or a static export โ plus a production checklist.
Core Web Vitals: What "Fast" Means
Google measures real-user experience with three Core Web Vitals. They affect search ranking and, more importantly, conversions.
- LCP โ Largest Contentful Paint (good โค 2.5s): when the main content (usually the hero image or heading) appears. Helped by server rendering,
next/imagewithpreload,next/font, streaming and CDN caching. - INP โ Interaction to Next Paint (good โค 200ms): how quickly the page responds to clicks and typing. Helped by shipping less client JavaScript (Server Components), code splitting (
next/dynamic),useTransition, and lazy third-party scripts. - CLS โ Cumulative Layout Shift (good โค 0.1): how much content jumps around while loading. Helped by
next/imagereserving space,next/fontsize-adjusted fallbacks, and skeletons that match the final layout.
Also watch TTFB (Time to First Byte) โ improved by static prerendering, caching and Partial Prerendering.
Measuring Performance
Measure before optimizing โ and measure the production build, never next dev (dev mode is intentionally slower).
- Lighthouse (Chrome DevTools โ Lighthouse) โ lab scores and concrete suggestions. Run it in an incognito window on
npm run build && npm run start. - PageSpeed Insights โ Lighthouse plus real-user data from the Chrome UX Report for your live URL.
- Vercel Speed Insights / Analytics โ real-user Core Web Vitals per route if you deploy on Vercel.
useReportWebVitalsโ send real-user metrics to your own analytics.
Note: Next.js 16 removed the size and First Load JS columns from next build output because they were inaccurate with Server Components. Use the bundle analyzer (section 3) and real-user metrics instead.
// app/components/WebVitals.jsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export default function WebVitals() {
useReportWebVitals((metric) => {
// metric.name: "LCP" | "INP" | "CLS" | "FCP" | "TTFB"
const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, page: location.pathname });
navigator.sendBeacon?.("/api/vitals", body) ||
fetch("/api/vitals", { method: "POST", body, keepalive: true });
});
return null;
}
// app/layout.js โ render <WebVitals /> once inside <body>Analyzing Bundles
Large client bundles hurt INP and load time. Find out what's inside:
npx next experimental-analyze(Next.js 16.1+) โ the built-in Turbopack bundle analyzer. It opens an interactive UI where you can filter modules, see their size in client and server bundles, and trace import chains to learn *why* a module was included. Add--outputto write results to.next/diagnostics/analyzefor comparing before/after a refactor.@next/bundle-analyzerโ the classic plugin, for projects building with Webpack (--webpack).
What to look for:
- A big library in the client bundle that's only needed on the server โ move that code into a Server Component.
- A Client Component boundary placed too high (Lecture 5) โ push
"use client"down. - Heavy widgets that aren't visible initially โ
next/dynamic. - Duplicate libraries doing the same job (two date libraries, two icon packs).
# Interactive analysis of the production bundles (Turbopack)
npx next experimental-analyze
# Save results to compare later
npx next experimental-analyze --output
cp -r .next/diagnostics/analyze ./analyze-before
# After your optimization, run again and compare the two foldersBuilt-in Optimizations You Can Turn On
optimizePackageImportsโ for libraries that export hundreds of modules (icon sets, utility libraries), Next.js only loads the modules you actually use. Many popular packages (e.g.lucide-react,date-fns,lodash-es) are optimized automatically; add others to the list.- React Compiler (
reactCompiler: true, stable in Next.js 16) โ automatically memoizes components and hooks, removing most manualuseMemo/useCallback/React.memo. Installbabel-plugin-react-compiler. Expect slightly slower builds. serverExternalPackagesโ keep heavy Node-only packages (e.g.sharp,puppeteer) out of the server bundle and load them fromnode_modulesat runtime.- Turbopack file-system cache โ
experimental.turbopackFileSystemCacheForDevkeeps compiler artifacts betweennext devrestarts for faster startup.
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
reactCompiler: true, // npm i -D babel-plugin-react-compiler
experimental: {
optimizePackageImports: ["@acme/icons", "my-ui-kit"],
turbopackFileSystemCacheForDev: true,
},
serverExternalPackages: ["sharp"],
};
export default nextConfig;Performance Patterns Recap
Most performance wins come from patterns you've already learned:
- Server Components by default; Client Components only at interactive leaves.
- Prerender what you can (static routes,
generateStaticParams,"use cache"), and stream the rest with<Suspense>. - Parallel data fetching with
Promise.all; avoid waterfalls; preload. next/imagewith correctsizesandpreloadon the LCP image.next/fontinstead of external font links.next/scriptwithlazyOnloadfor non-critical third parties.next/dynamicfor heavy, below-the-fold Client Components.<Link>prefetching for instant navigations;prefetch={false}for huge link lists.- Caching + revalidation so expensive work isn't repeated per request.
Running Work After the Response with after()
Some work shouldn't make the user wait: logging, analytics events, sending a notification, syncing to a search index. after() from next/server schedules a callback to run after the response has been sent (or after prerendering finishes).
It works in Server Components (including generateMetadata), Server Actions, Route Handlers and Proxy. Inside Server Actions and Route Handlers you can still read cookies() and headers() within the callback.
// app/actions/checkout.js
"use server";
import { after } from "next/server";
export async function placeOrder(cart) {
const order = await db.order.create({ data: cart }); // the user waits only for this
after(async () => {
await sendOrderConfirmationEmail(order.id); // runs after the response
await analytics.track("order_placed", { orderId: order.id, total: order.total });
});
return { orderId: order.id };
}Monitoring with instrumentation.js
instrumentation.js in the project root exports a register() function that runs once when a server instance starts โ the place to initialize monitoring tools like OpenTelemetry, Sentry or Datadog.
It can also export onRequestError, called whenever the server catches an error (in Server Components, Route Handlers, Server Actions or Proxy), with details about the route โ perfect for reporting errors with context.
instrumentation-client.js is the browser-side equivalent for client monitoring and analytics that must start before the app becomes interactive.
// instrumentation.js (project root)
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./monitoring/otel-node"); // initialize OpenTelemetry / Sentry for Node
}
}
export async function onRequestError(error, request, context) {
await fetch("https://logs.example.com/errors", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: error.message,
digest: error.digest, // matches error.digest shown in error.js
path: request.path,
routeType: context.routeType, // "render" | "route" | "action" | "proxy"
}),
});
}Testing Strategy for Next.js Apps
Use the testing pyramid โ many fast tests at the bottom, a few realistic ones at the top:
- Unit tests โ pure functions, Zod schemas, data transformers, utility helpers. Tools: Vitest or Jest.
- Component tests โ Client Components and synchronous Server Components rendered with React Testing Library.
- End-to-end (E2E) tests โ real browser against a running app: login flows, checkout, forms. Tools: Playwright or Cypress.
Important limitation: async Server Components are new to the React ecosystem, and unit-testing tools don't fully support rendering them yet. The official recommendation is to cover async Server Components with E2E tests, and to unit-test the data functions they call.
Unit & Component Tests with Vitest
Vitest is fast, ESM-native and has a Jest-compatible API. Set it up with the React plugin and jsdom, then test logic and Client Components.
# Install
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths
// vitest.config.mjs
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: { environment: "jsdom" },
});
// lib/__tests__/slugify.test.js โ unit test
import { expect, test } from "vitest";
import { slugify } from "../slugify";
test("creates URL-safe slugs", () => {
expect(slugify("Hello Next.js 16!")).toBe("hello-next-js-16");
});
// app/components/__tests__/Counter.test.jsx โ component test
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test } from "vitest";
import Counter from "../Counter";
test("increments on click", () => {
render(<Counter />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("button").textContent).toContain("1");
});
// package.json โ "test": "vitest"End-to-End Tests with Playwright
Playwright drives real Chromium, Firefox and WebKit browsers. Point it at your app and test complete user journeys โ the only way to fully test async Server Components, Server Actions, redirects and Proxy together.
Run E2E tests against a production build in CI for realistic behaviour. Playwright's webServer option can build and start the app automatically.
# Install
npm init playwright@latest
// playwright.config.js (excerpt)
export default {
webServer: {
command: "npm run build && npm run start",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
use: { baseURL: "http://localhost:3000" },
};
// e2e/contact.spec.js
import { test, expect } from "@playwright/test";
test("visitor can open notes and read a lecture", async ({ page }) => {
await page.goto("/notes");
await page.getByRole("link", { name: /Next\.js/ }).first().click();
await expect(page).toHaveURL(/\/notes\/nextjs/);
});
test("protected page redirects to login", async ({ page }) => {
await page.goto("/dashboard");
await expect(page).toHaveURL(/\/login/);
});Deployment Options
Next.js can run almost anywhere. Choose based on the features you need:
- Vercel โ zero-config deployments from Git, preview URLs for every pull request, global CDN, ISR and Cache Components handled automatically, built-in analytics. The easiest option.
- Node.js server โ
npm run buildthennpm run starton any VPS or PaaS (Railway, Render, DigitalOcean, AWS EC2). Supports all features. - Docker โ containerize the app (ideally with
output: "standalone") and run it on Kubernetes, AWS ECS, Google Cloud Run, Fly.io, etc. Supports all features. - Static export โ
output: "export"produces plain HTML/CSS/JS for any static host (S3, GitHub Pages, Netlify static). Limited: no server โ so no Server Actions, Route Handlers with dynamic logic, Proxy, ISR, cookies or on-demand image optimization. - Adapters โ platform-specific adapters (Netlify, Cloudflare, AWS Amplify, etc.) built on Next.js's Build Adapters API; feature support varies by platform.
Docker with Standalone Output
output: "standalone" makes next build produce .next/standalone โ a minimal folder containing only the files and node_modules the server actually needs, plus a small server.js. Docker images shrink from gigabytes to roughly 150โ250 MB.
Copy public/ and .next/static/ into the image yourself (or serve them from a CDN) โ the standalone folder doesn't include them by default.
Self-hosting notes:
- Image optimization uses
sharp, installed automatically by modern Next.js versions. - Running multiple instances? Set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYso all instances share the same Server Action encryption key, configure a shared cache handler (e.g. Redis) for consistent caching, and set adeploymentIdto handle version skew between deployments.
// next.config.mjs
const nextConfig = { output: "standalone" };
export default nextConfig;
# Dockerfile (multi-stage)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
# Build & run
# docker build -t my-next-app .
# docker run -p 3000:3000 --env-file .env.production my-next-appDeploying to Vercel & CI Basics
Vercel in four steps:
- Push your project to GitHub/GitLab/Bitbucket.
- Import the repository at vercel.com โ framework is detected automatically.
- Add environment variables (e.g.
EMAIL_USER,EMAIL_PASS,SESSION_SECRET) in Project Settings โ they're not read from your local.envfiles. - Deploy. Every push to
maindeploys production; every pull request gets a preview URL.
A minimal CI pipeline (GitHub Actions or similar) should run on every pull request: install โ lint (next build no longer lints in v16) โ type-check โ unit tests โ build โ E2E tests. Cache .next/cache between runs to speed up builds.
# .github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- uses: actions/cache@v4
with:
path: .next/cache
key: nextjs-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
restore-keys: nextjs-${{ hashFiles('package-lock.json') }}-
- run: npm ci
- run: npx eslint .
- run: npx vitest run
- run: npm run build
- run: npx playwright install --with-deps && npx playwright testProduction Checklist
Before launch, go through this list:
- Build & run locally with
npm run build && npm run start; fix every build warning. - Environment variables set on the host (not just locally); no secrets in
NEXT_PUBLIC_variables;.envfiles not committed. - Error handling:
error.js,global-error.jsandnot-found.jsexist and look good; errors are reported (onRequestError, Sentry). - Security: auth checks in every Server Action and Route Handler; security headers; rate limiting on public forms; dependencies updated.
- SEO: unique titles/descriptions, canonical URLs, Open Graph images,
sitemap.js,robots.js, JSON-LD; staging isnoindex. - Performance: Lighthouse โฅ 90 on key pages; LCP image preloaded; fonts via
next/font; third-party scripts deferred; bundle analyzed. - Caching: correct static vs. dynamic routes in the build output; revalidation wired to your CMS/admin actions.
- Accessibility: semantic HTML, alt text, keyboard navigation, focus states, sufficient contrast.
- Monitoring: analytics and Core Web Vitals collection; uptime checks.
- Forms tested end-to-end in production โ e.g. submit your contact form on the live site and confirm the email arrives.
Core Course Wrap-Up: What You've Learned
Congratulations โ you've completed the core Next.js 16 course! You can now:
- Set up and structure a Next.js 16 project with the App Router and Turbopack (Lecture 1).
- Build any routing structure โ layouts, dynamic and catch-all routes, groups, parallel and intercepting routes (Lectures 2โ4).
- Design with Server and Client Components, keeping JavaScript minimal (Lecture 5).
- Fetch data efficiently with streaming, parallel requests and memoization (Lecture 6).
- Cache and revalidate with Cache Components,
"use cache",cacheLife,cacheTagandupdateTag(Lecture 7). - Mutate data with Server Actions and accessible, progressively enhanced forms (Lecture 8).
- Build APIs with Route Handlers and request logic with Proxy (Lecture 9).
- Style and optimize images, fonts and scripts (Lecture 10), and make pages discoverable with metadata and SEO (Lecture 11).
- Secure your app with sessions, a Data Access Layer and proper authorization (Lecture 12).
- Measure, test and deploy to production (Lecture 13).
Keep going with the advanced lectures: internationalization (14), databases end to end (15), MDX, CMS and Draft Mode (16), View Transitions and React 19.2 (17), and static exports, SPAs and PWAs (18).
Then build something real. A portfolio with a blog and notes section (like this site), a SaaS dashboard with auth, or an e-commerce store will exercise every lecture. Keep the official docs bundled in node_modules/next/dist/docs handy โ they always match your installed version.