Next.js Production — Từ Dev Đến Triển Khai Thực Tế
Next.js best practices cho production: App Router, server actions, caching, ISR, middleware và tối ưu performance.
Next.js là React framework phổ biến nhất cho production. App Router (2023) thay đổi cách tổ chức code hoàn toàn.
App Router vs Pages Router#
| Pages Router | App Router |
|---|---|
| File-based routing | File-based routing + layout |
_app.tsx global | layout.tsx per segment |
getServerSideProps | Server Components (mặc định) |
| Client Components khi cần | 'use client' khi cần |
| API routes riêng | Server Actions + Route Handlers |
Cấu Trúc Project#
src/
├── app/
│ ├── layout.tsx // Root layout
│ ├── page.tsx // Home page
│ ├── loading.tsx // Loading UI
│ ├── error.tsx // Error boundary
│ ├── not-found.tsx // 404
│ ├── blog/
│ │ ├── [slug]/
│ │ │ ├── page.tsx
│ │ │ └── loading.tsx
│ │ └── page.tsx
│ └── api/
│ └── posts/
│ └── route.ts // API Route Handler
├── components/
├── lib/
└── styles/txtServer Components — Mặc Định#
// Mặc định là Server Component — không cần 'use client'
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.posts.findUnique({
where: { slug: params.slug },
include: { author: true },
});
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author.name}</p>
<div>{post.content}</div>
</article>
);
}tsxLợi ích: Direct database access, không bundle JS cho server code, nhỏ hơn, nhanh hơn.
Client Components — Khi Cần Tương Tác#
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
const router = useRouter();
async function handleLike() {
await fetch('/api/like', {
method: 'POST',
body: JSON.stringify({ postId }),
});
setLiked(true);
router.refresh(); // Re-render Server Components
}
return (
<button onClick={handleLike} className={liked ? 'liked' : ''}>
{liked ? '❤️' : '🤍'}
</button>
);
}tsxNguyên tắc: Server Component mặc định, chỉ thêm 'use client' khi cần state, effect, event handler.
Server Actions — Không Cần API Route#
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validate
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' };
}
// DB
await db.posts.create({ data: { title, content } });
// Revalidate cache
revalidatePath('/blog');
return { success: true };
}tsx// Component
export function PostForm() {
return (
<form action={createPost}>
<input name="title" required minLength={3} />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}tsxKhông cần API route — logic chạy trên server, an toàn, type-safe.
Data Fetching & Caching#
// Static — fetch tại build time (SSG)
async function getStaticPost(slug: string) {
const post = await fetch(`http://.../posts/${slug}`, {
next: { revalidate: 3600 }, // ISR — revalidate mỗi giờ
});
return post.json();
}
// Dynamic — fetch mỗi request (SSR)
async function getDynamicPost(slug: string) {
const post = await fetch(`http://.../posts/${slug}`, {
cache: 'no-store', // Không cache
});
return post.json();
}
// Incremental Static Regeneration (ISR)
export const revalidate = 3600; // Revalidate page mỗi giờ
export const dynamic = 'force-static'; // Static by defaulttsxMiddleware — Bảo Vệ Route#
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
// Redirect nếu chưa login
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add header
const response = NextResponse.next();
response.headers.set('x-pathname', request.nextUrl.pathname);
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};tsxImage Optimization#
import Image from 'next/image';
export default function Profile({ user }: { user: User }) {
return (
<Image
src={user.avatar}
alt={user.name}
width={300}
height={300}
priority={true} // Above the fold
placeholder="blur" // Blur placeholder
blurDataURL="data:..." // Base64 blur
/>
);
}tsxNext.js tự động: WebP, responsive sizes, lazy loading, preload priority images.
Metadata & SEO#
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'My Blog',
template: '%s | My Blog',
},
description: 'Tech blog about programming',
openGraph: {
title: 'My Blog',
description: 'Tech blog about programming',
type: 'website',
},
robots: {
index: true,
follow: true,
},
};
// Per-page metadata
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.thumbnail] },
};
}tsxPerformance Checklist#
- Server Components mặc định
-
next/imagecho tất cả images -
next/linkcho client-side navigation - Streaming với
loading.tsxvàSuspense - ISR cho content pages
- Static rendering cho public pages
-
next/font— tối ưu font (không render-blocking) - Bundle analyzer — kiểm tra kích thước
- Analytics — Web Vitals tracking
Deploy#
# Build
npm run build
// vercel.json
{
"framework": "nextjs",
"buildCommand": "npm run build"
}
// Docker
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["npm", "start"]bashKết Luận#
Next.js App Router là tương lai của React. Server Components + Server Actions giảm đáng kể JavaScript bundle. ISR cho content site, dynamic cho personalized pages. Middleware cho auth và redirects. Deploy lên Vercel hoặc Docker.