Next.js 16 from Zero to Senior · Part 10 — Production, Testing & Debugging
Ship like a senior: performance budgets and bundle analysis, testing with Vitest and Playwright, deploying to Vercel and self-hosting with Docker (standalone output), debugging with the Next Devtools MCP, and a capstone project.
You can now build almost anything in Next.js 16 {Giờ bạn có thể dựng gần như mọi thứ trong Next.js 16}. This final part is about the difference between “it works on my machine” and “it’s reliable in production” — performance, testing, deployment, and the debugging skills that separate seniors from everyone else {Phần cuối này nói về khác biệt giữa “chạy trên máy tôi” và “đáng tin trên production” — hiệu năng, test, deploy, và kỹ năng debug tách senior khỏi phần còn lại}.
1. Production builds & bundle analysis {Build production & phân tích bundle}
next build runs Turbopack to produce an optimized build and prints a per-route report {next build chạy Turbopack tạo build tối ưu và in báo cáo theo route}:
npm run build
Route (app) Size First Load JS
┌ ○ / 1.2 kB 98 kB
├ ◐ /product/[id] 2.1 kB 110 kB
└ ƒ /dashboard 3.4 kB 120 kB
○ (Static) prerendered as static content
◐ (PPR) partial prerender — static shell + dynamic holes
ƒ (Dynamic) server-rendered on demand
Read the legend {Đọc chú thích}: ○ static, ◐ partial-prerendered (PPR), ƒ dynamic {○ tĩnh, ◐ prerender một phần (PPR), ƒ động}. If a route you expected to be static shows ƒ, something is reading the request — track it down {Nếu một route bạn nghĩ là tĩnh lại hiện ƒ, có gì đó đang đọc request — truy tìm nó}.
To see what’s in your JS, run the bundle analyzer {Để xem cái gì trong JS, chạy bundle analyzer}:
npm i -D @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer';
const analyze = withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true' });
export default analyze(nextConfig);
ANALYZE=true npm run build # opens an interactive treemap
Hunt for accidentally-client-side heavy libraries (date pickers, charting, markdown renderers) and move them server-side or lazy-load them {Săn các thư viện nặng vô tình ở client (date picker, vẽ chart, render markdown) và chuyển server-side hoặc lazy-load}.
2. Performance budget {Ngân sách hiệu năng}
Seniors set numbers, not vibes {Senior đặt con số, không cảm tính}:
- First Load JS under ~120 kB per route; investigate anything larger {First Load JS dưới ~120 kB mỗi route; điều tra cái nào lớn hơn}.
- Core Web Vitals — LCP < 2.5s, CLS < 0.1, INP < 200ms {Core Web Vitals — LCP < 2.5s, CLS < 0.1, INP < 200ms}.
- Keep
'use client'at the leaves; default to Server Components (Part 5) {Giữ'use client'ở các lá; mặc định Server Components (Phần 5)}. - Cache the shared/slow-changing; stream the personal/fast-changing (Part 4) {Cache thứ dùng chung/ít đổi; stream thứ cá nhân/đổi nhanh (Phần 4)}.
Lazy-load heavy client components with next/dynamic {Lazy-load component client nặng bằng next/dynamic}:
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./chart'), {
loading: () => <ChartSkeleton />,
});
3. Testing strategy {Chiến lược test}
A pragmatic pyramid for Next.js apps {Một kim tự tháp thực dụng cho app Next.js}:
- Unit — pure functions, validation schemas, DAL logic {Unit — hàm thuần, schema validate, logic DAL}.
- Component — Client Components in isolation {Component — Client Component cô lập}.
- E2E — the critical user flows through a real browser {E2E — luồng user quan trọng qua trình duyệt thật}.
Unit & component tests with Vitest {Unit & component test với Vitest}
npm i -D vitest @testing-library/react @testing-library/jest-dom jsdom
// counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './counter';
test('increments', async () => {
render(<Counter />);
await userEvent.click(screen.getByRole('button'));
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
Async Server Components aren’t fully testable in jsdom yet — test the data functions they call (unit) and cover the rendered result with E2E {Async Server Component chưa test đầy đủ được trong jsdom — hãy test các hàm dữ liệu chúng gọi (unit) và phủ kết quả render bằng E2E}.
E2E with Playwright {E2E với Playwright}
npm init playwright@latest
// e2e/todos.spec.ts
import { test, expect } from '@playwright/test';
test('user can add a todo', async ({ page }) => {
await page.goto('/todos');
await page.getByRole('textbox', { name: /title/i }).fill('Ship it');
await page.getByRole('button', { name: /add/i }).click();
await expect(page.getByText('Ship it')).toBeVisible();
});
Playwright exercises the full stack: Server Components, Server Actions, revalidation — exactly what users hit {Playwright chạy toàn bộ stack: Server Components, Server Actions, revalidation — đúng thứ user gặp}.
4. Deploying to Vercel {Deploy lên Vercel}
The zero-config path: push to Git, import the repo in Vercel, set env vars {Đường không-cấu-hình: push lên Git, import repo trong Vercel, set env var}. Vercel maps Next.js features automatically — static pages to the CDN, PPR shells to the edge, dynamic routes and Server Actions to serverless functions, proxy.ts to the edge/runtime {Vercel ánh xạ tính năng Next.js tự động — trang tĩnh tới CDN, vỏ PPR tới edge, route động và Server Action tới serverless function, proxy.ts tới edge/runtime}. Cache tags and revalidateTag work out of the box {Cache tag và revalidateTag chạy ngay}.
5. Self-hosting with Docker {Tự host với Docker}
Next.js runs anywhere Node runs {Next.js chạy ở mọi nơi Node chạy}. For containers, enable standalone output so the image only contains what it needs {Cho container, bật standalone output để image chỉ chứa thứ cần thiết}:
// next.config.ts
const nextConfig: NextConfig = {
output: 'standalone',
};
# Dockerfile (multi-stage)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.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
# standalone output bundles a minimal server + only required deps
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t senior-next .
docker run -p 3000:3000 --env-file .env.production senior-next
Next.js 16 changed some container memory behavior — set realistic memory limits and test under load, or long-running containers can be surprised in production {Next.js 16 đã thay đổi vài hành vi bộ nhớ trong container — đặt giới hạn bộ nhớ thực tế và test dưới tải, nếu không container chạy lâu có thể “bất ngờ” trên production}. Put Nginx in front for TLS and static caching (see the Nginx series on this blog) {Đặt Nginx phía trước cho TLS và cache tĩnh (xem series Nginx trên blog này)}.
6. Observability {Quan sát}
instrumentation.ts— a root file that runs once on server startup; wire up OpenTelemetry / Sentry here {instrumentation.ts— file gốc chạy một lần khi server khởi động; gắn OpenTelemetry / Sentry ở đây}.- Web Vitals — report real-user metrics with the
useReportWebVitalshook {Web Vitals — báo cáo số liệu người-dùng-thật bằng hookuseReportWebVitals}. - Structured logs — Next.js 16 improved build/request logging; ship logs to your platform and alert on error rates {Log có cấu trúc — Next.js 16 cải thiện log build/request; gửi log tới nền tảng của bạn và cảnh báo theo tỉ lệ lỗi}.
7. Debugging with the Next Devtools MCP {Debug với Next Devtools MCP}
Next.js 16 ships a Devtools MCP server — a Model Context Protocol integration that lets AI tools (and you) inspect the running app: route info, render modes, cache behavior, and build diagnostics {Next.js 16 có Devtools MCP server — tích hợp Model Context Protocol cho phép công cụ AI (và bạn) soi app đang chạy: thông tin route, chế độ render, hành vi cache, và chẩn đoán build}. Connect it to your editor’s AI assistant to ask “why is /dashboard rendering dynamically?” and get a grounded answer from the actual app {Kết nối nó với trợ lý AI trong editor để hỏi “tại sao /dashboard render động?” và nhận câu trả lời có căn cứ từ chính app}.
The bugs you’ll actually hit {Những bug bạn sẽ thực sự gặp}
| Symptom {Triệu chứng} | Likely cause {Nguyên nhân khả dĩ} |
|---|---|
| “params should be awaited” error {Lỗi “params should be awaited”} | Reading params/searchParams/cookies() synchronously — await them (Part 2) {Đọc đồng bộ — hãy await (Phần 2)} |
| Hydration mismatch {Không khớp hydration} | Date/random/localStorage in a Client Component — defer to useEffect (Part 5) {Date/random/localStorage trong Client Component — hoãn vào useEffect (Phần 5)} |
| Stale data after a mutation {Data cũ sau mutation} | Forgot revalidateTag/updateTag/revalidatePath (Parts 4, 6) {Quên revalidateTag/updateTag/revalidatePath (Phần 4, 6)} |
'use cache' does nothing {'use cache' vô tác dụng} | cacheComponents: true not enabled (Part 4) {Chưa bật cacheComponents: true (Phần 4)} |
| Secret leaked to browser {Secret lộ ra trình duyệt} | Missing server-only, or wrong NEXT_PUBLIC_ prefix (Parts 3, 9) {Thiếu server-only, hoặc sai tiền tố NEXT_PUBLIC_ (Phần 3, 9)} |
Route unexpectedly dynamic (ƒ) {Route động ngoài ý muốn (ƒ)} | An uncached cookies()/headers() read not wrapped in <Suspense> (Part 8) {Đọc cookies()/headers() không cache, không bọc <Suspense> (Phần 8)} |
| 500 on a Server Action {500 trên một Server Action} | Unvalidated input or a swallowed redirect() (Part 6) {Input không validate hoặc redirect() bị nuốt (Phần 6)} |
Knowing this table is a big part of being senior — most production incidents are one of these {Biết bảng này chính là một phần lớn của việc làm senior — phần lớn sự cố production là một trong số này}.
8. Capstone project {Dự án capstone}
Build a small but complete app that uses every concept in the series {Dựng một app nhỏ nhưng hoàn chỉnh dùng mọi khái niệm trong series}. Suggested: a “Links” dashboard (a personal bookmark manager) {Gợi ý: một dashboard “Links” (trình quản lý bookmark cá nhân)}.
Requirements {Yêu cầu}:
- Public marketing page at
/— static, with metadata + OG image (Parts 1, 8) {Trang marketing công khai tại/— tĩnh, có metadata + ảnh OG (Phần 1, 8)}. - Auth — login/logout with httpOnly session cookies and a DAL (Part 9) {Auth — đăng nhập/xuất với cookie session httpOnly và một DAL (Phần 9)}.
proxy.tsredirecting logged-out users from/app(Parts 7, 9) {proxy.tschuyển hướng user chưa đăng nhập khỏi/app(Phần 7, 9)}.- Dashboard at
/app— a PPR page: cached stats shell + a streamed personalized feed (Parts 3, 4) {Dashboard tại/app— một trang PPR: vỏ thống kê đã cache + feed cá nhân hóa được stream (Phần 3, 4)}. - CRUD links via Server Actions with zod validation, optimistic UI, and
updateTag(Parts 4, 6) {CRUD link qua Server Actions với validate zod, UI lạc quan, vàupdateTag(Phần 4, 6)}. - Public JSON API at
/api/linksfor a hypothetical mobile client (Part 7) {API JSON công khai tại/api/linkscho một client mobile giả định (Phần 7)}. - URL-driven filtering of links via
searchParams(Part 5) {Lọc theo URL các link quasearchParams(Phần 5)}. - Tests — Vitest for the DAL + a Playwright E2E for “add a link” (this part) {Test — Vitest cho DAL + một Playwright E2E cho “thêm một link” (phần này)}.
- Deploy — to Vercel and as a Docker image with
output: 'standalone'(this part) {Deploy — lên Vercel và dưới dạng Docker image vớioutput: 'standalone'(phần này)}.
If you can build this end to end and explain why each rendering and caching decision was made, you’re operating at a senior level on modern Next.js {Nếu bạn dựng được cái này từ đầu đến cuối và giải thích vì sao mỗi quyết định render và cache được đưa ra, bạn đang ở trình độ senior trên Next.js hiện đại}.
9. Where to go next {Đi đâu tiếp}
- Read the things you used most in production: the caching, rendering, and proxy docs {Đọc lại những thứ bạn dùng nhiều nhất trên production: docs về caching, rendering, và proxy}.
- Rebuild a side project you already have on the App Router with the Next.js 16 model {Dựng lại một side project bạn đã có trên App Router với mô hình Next.js 16}.
- Read source: the Vercel
commercetemplate is a real-world App Router codebase using Server Components + Server Actions {Đọc source: templatecommercecủa Vercel là một codebase App Router thực tế dùng Server Components + Server Actions}.
Series recap {Tóm tắt series}
1 Foundations & App Router — server-first, file routing, 'use client'
2 Routing deep dive — dynamic, groups, parallel/intercepting, layouts
3 Server Components & fetching — async data, no waterfalls, streaming
4 Cache Components — 'use cache', cacheLife, cacheTag, PPR
5 Client Components & URL state — minimal client, URL as state
6 Server Actions & forms — mutations, validation, optimistic, revalidate
7 Route Handlers & Proxy — APIs, cookies/headers, proxy.ts
8 Rendering, metadata & assets — SEO, OG, image/font/script
9 Auth, sessions & security — cookies, DAL, headers
10 Production & debugging — perf, tests, deploy, the bug table
You started not knowing what an App Router was {Bạn bắt đầu khi chưa biết App Router là gì}. You now understand server-first rendering, the explicit Next.js 16 caching model, mutations, APIs, auth, and how to ship and debug it all {Giờ bạn hiểu render ưu tiên server, mô hình caching rõ ràng của Next.js 16, mutation, API, auth, và cách ship cùng debug tất cả}. That’s the senior toolkit {Đó là bộ công cụ của senior}. Now go build something real {Giờ hãy đi dựng thứ gì đó thật}.