jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 useReportWebVitals hook {Web Vitals — báo cáo số liệu người-dùng-thật bằng hook useReportWebVitals}.
  • 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}:

  1. 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)}.
  2. 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)}.
  3. proxy.ts redirecting logged-out users from /app (Parts 7, 9) {proxy.ts chuyển hướng user chưa đăng nhập khỏi /app (Phần 7, 9)}.
  4. 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)}.
  5. 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)}.
  6. Public JSON API at /api/links for a hypothetical mobile client (Part 7) {API JSON công khai tại /api/links cho một client mobile giả định (Phần 7)}.
  7. URL-driven filtering of links via searchParams (Part 5) {Lọc theo URL các link qua searchParams (Phần 5)}.
  8. 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)}.
  9. Deploy — to Vercel and as a Docker image with output: 'standalone' (this part) {Deploy — lên Vercel dưới dạng Docker image với output: '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 commerce template is a real-world App Router codebase using Server Components + Server Actions {Đọc source: template commerce củ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}.