Next.js Core Concepts — From Pages Router to App Router (v14 → v15 → v16)
A comprehensive bilingual guide covering rendering, routing, caching in Next.js across versions 14, 15, and 16. Written in English with inline Vietnamese translations for language learners.
Introduction {Giới thiệu}
If you’ve only ever used Next.js with the Pages Router {Nếu bạn chỉ mới dùng Next.js với Pages Router}, this post is for you {bài này dành cho bạn}. The framework has evolved dramatically from version 14 to 16 {Framework đã thay đổi rất nhiều từ phiên bản 14 đến 16}, and the mental model you built around getStaticProps and getServerSideProps no longer applies {và mô hình tư duy bạn xây dựng xung quanh getStaticProps và getServerSideProps không còn áp dụng được nữa}.
We’ll cover {Chúng ta sẽ đi qua}:
- Routing — how file-based routing works in the App Router {cách routing dựa trên file hoạt động trong App Router}
- Rendering — SSG, SSR, ISR, Streaming, CSR, PPR {các chiến lược render}
- Caching — how it changed from “cache everything” to “cache nothing” to “cache explicitly” {cách caching thay đổi từ “cache mọi thứ” sang “không cache gì” sang “cache có chủ đích”}
- Version differences — what broke, what improved across v14 → v15 → v16 {những gì thay đổi, cải thiện qua các phiên bản}
This post uses bilingual format {Bài viết sử dụng định dạng song ngữ}: English first, Vietnamese in curly braces {} {tiếng Anh trước, tiếng Việt trong ngoặc nhọn {}}. Read naturally in English, glance at Vietnamese for context {Đọc tự nhiên bằng tiếng Anh, liếc qua tiếng Việt để hiểu ngữ cảnh}.
Pages Router vs App Router {Pages Router và App Router}
The Old World: Pages Router {Thế giới cũ: Pages Router}
In the Pages Router {Trong Pages Router}, every file in pages/ became a route {mọi file trong pages/ trở thành một route}:
pages/
├── index.tsx → /
├── about.tsx → /about
├── blog/
│ ├── index.tsx → /blog
│ └── [slug].tsx → /blog/:slug
└── api/
└── hello.ts → /api/hello
Data fetching was done through special functions {Việc lấy dữ liệu được thực hiện qua các hàm đặc biệt}:
// pages/blog/[slug].tsx — Pages Router
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post }, revalidate: 60 };
}
export async function getStaticPaths() {
const slugs = await getAllSlugs();
return {
paths: slugs.map((slug) => ({ params: { slug } })),
fallback: 'blocking',
};
}
export default function BlogPost({ post }) {
return <article>{post.content}</article>;
}
Problems with this model {Vấn đề của mô hình này}:
- All components are client components by default {Tất cả component mặc định là client component} — they ship JavaScript to the browser {chúng gửi JavaScript đến trình duyệt}
- Data fetching is page-level only {Lấy dữ liệu chỉ ở cấp page} — you can’t fetch in a nested component without prop drilling or client-side fetching {bạn không thể fetch trong component lồng nhau mà không truyền prop hoặc fetch phía client}
- Layouts require workarounds {Layout cần giải pháp tạm} — no native nested layouts {không có nested layout native}
The New World: App Router {Thế giới mới: App Router}
Introduced in Next.js 13, stabilized in 14 {Được giới thiệu ở Next.js 13, ổn định ở 14}, the App Router flips everything {App Router đảo ngược mọi thứ}:
app/
├── layout.tsx → Root layout (wraps everything)
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── layout.tsx → Blog layout (persists across blog pages)
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── api/
└── hello/
└── route.ts → /api/hello
Key paradigm shifts {Những thay đổi mô hình chính}:
| Pages Router | App Router |
|---|---|
| All components are Client Components {Tất cả là Client Component} | All components are Server Components by default {Mặc định là Server Component} |
getStaticProps / getServerSideProps | Just async functions — fetch directly in components {Chỉ cần hàm async — fetch trực tiếp trong component} |
| No nested layouts {Không có nested layout} | Native nested layouts {Nested layout native} |
_app.tsx + _document.tsx | layout.tsx at any level {layout.tsx ở bất kỳ cấp nào} |
API routes in pages/api/ | Route Handlers in app/api/.../route.ts |
// app/blog/[slug]/page.tsx — App Router
// This is a Server Component by default {Đây là Server Component mặc định}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // async in v15+
const post = await fetchPost(slug);
return <article>{post.content}</article>;
}
Notice {Chú ý}: no getStaticProps, no export default function receiving props from a data function {không cần getStaticProps, không cần export default function nhận props từ hàm data}. The component itself IS the data layer {Component chính nó LÀ tầng data}.
Routing Deep-Dive {Routing chi tiết}
File Conventions {Quy ước file}
The App Router uses special filenames to define UI structure {App Router dùng tên file đặc biệt để định nghĩa cấu trúc UI}:
| File | Purpose {Mục đích} |
|---|---|
page.tsx | The UI for a route — makes the route publicly accessible {UI cho route — làm route có thể truy cập} |
layout.tsx | Shared UI that wraps children — persists across navigations {UI dùng chung bọc children — tồn tại qua các lần navigate} |
loading.tsx | Loading UI shown while content streams in {UI loading hiển thị khi content đang stream} |
error.tsx | Error boundary for a route segment {Error boundary cho route segment} |
not-found.tsx | UI for 404 within a segment {UI cho 404 trong segment} |
template.tsx | Like layout but re-mounts on navigation {Giống layout nhưng re-mount khi navigate} |
default.tsx | Fallback UI for parallel routes {UI mặc định cho parallel route} |
route.ts | API endpoint (GET, POST, etc.) {API endpoint} |
Layouts — The Killer Feature {Layouts — Tính năng đỉnh nhất}
Layouts persist across page navigations {Layout tồn tại xuyên suốt các lần chuyển trang}. They don’t re-render when you navigate between sibling pages {Chúng không re-render khi bạn navigate giữa các trang cùng cấp}:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar /> {/* Never re-renders on navigation */}
<main className="flex-1">{children}</main>
</div>
);
}
Layouts can be nested {Layout có thể lồng nhau}:
app/
├── layout.tsx → Root: html, body, fonts, providers
├── (marketing)/
│ ├── layout.tsx → Marketing: header, footer
│ ├── page.tsx → /
│ └── pricing/page.tsx → /pricing
└── (dashboard)/
├── layout.tsx → Dashboard: sidebar, auth check
├── page.tsx → /dashboard (if you set it up)
└── settings/page.tsx → /settings
Route Groups {Nhóm route}
Wrap folders in parentheses () to organize without affecting the URL {Bọc folder trong ngoặc tròn () để tổ chức mà không ảnh hưởng URL}:
app/
├── (auth)/
│ ├── login/page.tsx → /login (not /auth/login)
│ └── register/page.tsx → /register
├── (shop)/
│ ├── layout.tsx → Different layout for shop pages
│ ├── products/page.tsx → /products
│ └── cart/page.tsx → /cart
This lets you have multiple root layouts {Điều này cho phép bạn có nhiều root layout} — different headers/footers for different sections of your app {header/footer khác nhau cho các phần khác nhau của app}.
Dynamic Routes {Route động}
app/blog/[slug]/page.tsx → /blog/hello-world
app/shop/[...slugs]/page.tsx → /shop/a/b/c (catch-all)
app/docs/[[...slugs]]/page.tsx → /docs OR /docs/a/b (optional catch-all)
// app/blog/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// In v14: params was synchronous → params.slug
// In v15+: params is async → await params, then destructure
return <h1>{slug}</h1>;
}
Parallel Routes {Route song song}
Render multiple pages in the same layout simultaneously {Render nhiều page trong cùng layout đồng thời}:
app/dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx → Analytics panel
│ └── default.tsx → Fallback when not matched
├── @team/
│ ├── page.tsx → Team panel
│ └── default.tsx
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2">
{analytics}
{team}
</div>
</div>
);
}
Use cases {Trường hợp sử dụng}: dashboards, modals that preserve URL state, conditional rendering based on auth {dashboard, modal giữ URL state, render có điều kiện dựa trên auth}.
Intercepting Routes {Route chặn}
Show a route in a different context {Hiển thị route trong ngữ cảnh khác} — like opening a photo in a modal while the URL updates {như mở ảnh trong modal trong khi URL cập nhật}:
app/
├── feed/
│ ├── page.tsx
│ └── (..)photo/[id]/ → Intercepts /photo/[id] from feed
│ └── page.tsx → Shows in modal
└── photo/[id]/
└── page.tsx → Full page (direct navigation or refresh)
Convention {Quy ước}: (.) same level, (..) one level up, (..)(..) two levels up, (...) from root.
Middleware → Proxy (v16) {Middleware → Proxy (v16)}
In v14-15, middleware.ts ran at the edge {Ở v14-15, middleware.ts chạy ở edge}:
// middleware.ts (v14-15)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (!request.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};
In v16, this becomes proxy.ts {Ở v16, đổi thành proxy.ts} — running on Node.js runtime by default {chạy trên Node.js runtime mặc định}:
// proxy.ts (v16)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
if (!request.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*'],
};
Why the rename {Tại sao đổi tên}? “Middleware” implied it could do anything {gợi ý nó có thể làm mọi thứ}. In reality, it’s a network proxy {Thực tế, nó là proxy mạng} — it intercepts requests before they reach your app {nó chặn request trước khi chúng đến app}. The new name clarifies its role {Tên mới làm rõ vai trò}. Plus, running on Node.js gives access to fs, crypto, and other Node APIs {Thêm nữa, chạy trên Node.js cho phép truy cập fs, crypto, và các Node API khác}.
Rendering Strategies {Các chiến lược Render}
This is where Next.js gets complex but powerful {Đây là nơi Next.js trở nên phức tạp nhưng mạnh mẽ}. Let’s go through each strategy {Hãy đi qua từng chiến lược}:
Server Components vs Client Components {Server Component và Client Component}
The fundamental building block {Khối xây dựng cơ bản} of the App Router:
// Server Component (default) — NO "use client" directive
export default async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
);
}
// Client Component — needs interactivity
'use client';
import { useState } from 'react';
export default function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Add to cart ({count})
</button>
);
}
When to use which {Khi nào dùng cái nào}:
| Server Component | Client Component |
|---|---|
| Fetch data {Lấy dữ liệu} | Event handlers (onClick, onChange) {Xử lý sự kiện} |
| Access backend directly {Truy cập backend trực tiếp} | useState, useEffect, hooks |
| Keep secrets safe {Giữ bí mật an toàn} | Browser APIs (localStorage, window) |
| Reduce client bundle {Giảm bundle phía client} | Third-party interactive libraries {Thư viện tương tác bên thứ 3} |
SSG — Static Site Generation {Tạo trang tĩnh}
Pages are rendered at build time {Trang được render lúc build}. The HTML is generated once and served from CDN {HTML được tạo một lần và phục vụ từ CDN}.
// app/blog/[slug]/page.tsx — statically generated at build time
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
When it’s chosen {Khi nào được chọn}: Next.js automatically uses SSG when your page has no dynamic functions (cookies(), headers(), searchParams) and all data is cacheable {Next.js tự động dùng SSG khi page không có hàm động và tất cả data có thể cache}.
Equivalent in Pages Router {Tương đương ở Pages Router}: getStaticProps + getStaticPaths.
SSR — Server-Side Rendering {Render phía server}
Pages are rendered on every request {Trang được render mỗi request}. Fresh data every time, but slower {Dữ liệu mới mỗi lần, nhưng chậm hơn}.
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
// Using cookies() makes this page dynamic (SSR)
const token = (await cookies()).get('session');
const data = await fetchDashboardData(token?.value);
return <DashboardUI data={data} />;
}
You can also force SSR explicitly {Bạn cũng có thể ép SSR rõ ràng}:
// Force dynamic rendering
export const dynamic = 'force-dynamic';
Equivalent in Pages Router {Tương đương ở Pages Router}: getServerSideProps.
ISR — Incremental Static Regeneration {Tái tạo tĩnh tăng dần}
The best of both worlds {Tốt nhất của cả hai}: static speed + background updates {tốc độ tĩnh + cập nhật nền}:
// app/products/page.tsx
// Revalidate every 60 seconds
export const revalidate = 60;
export default async function Products() {
const products = await fetch('https://api.store.com/products');
return <ProductGrid products={products} />;
}
How it works {Cách hoạt động}:
- First request → serves cached version {Request đầu → phục vụ bản cache}
- After 60s, next request still serves cache BUT triggers background regeneration {Sau 60s, request tiếp vẫn phục vụ cache NHƯNG kích hoạt tái tạo nền}
- Once regeneration completes, new version replaces old cache {Khi tái tạo xong, bản mới thay thế cache cũ}
On-demand revalidation {Revalidation theo yêu cầu}:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) revalidatePath(path);
if (tag) revalidateTag(tag);
return Response.json({ revalidated: true });
}
Equivalent in Pages Router {Tương đương ở Pages Router}: getStaticProps with revalidate option.
Streaming SSR {Render server theo luồng}
Instead of waiting for all data before sending HTML {Thay vì đợi tất cả data trước khi gửi HTML}, stream parts as they become ready {stream từng phần khi chúng sẵn sàng}:
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<StaticHeader />
{/* Streams in when data is ready */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
async function SlowDataComponent() {
// Takes 2 seconds — but doesn't block the whole page
const data = await fetchSlowAPI();
return <DataTable data={data} />;
}
The loading.tsx convention is syntactic sugar for Suspense {Quy ước loading.tsx là cú pháp rút gọn cho Suspense}:
// app/dashboard/loading.tsx — Automatically wraps page.tsx in Suspense
export default function Loading() {
return <DashboardSkeleton />;
}
CSR — Client-Side Rendering {Render phía client}
For highly interactive UIs that don’t need SEO {Cho UI tương tác cao không cần SEO}:
'use client';
import { useEffect, useState } from 'react';
export default function LiveChat() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://chat.example.com');
ws.onmessage = (e) => {
setMessages((prev) => [...prev, JSON.parse(e.data)]);
};
return () => ws.close();
}, []);
return <ChatUI messages={messages} />;
}
PPR — Partial Prerendering {Render trước một phần}
The future of rendering {Tương lai của rendering}. PPR combines static shells with dynamic streaming {PPR kết hợp shell tĩnh với streaming động}:
┌──────────────────────────────────────────────┐
│ Static Shell (served from CDN instantly) │
│ ┌────────────────────────────────────────┐ │
│ │ Header, Navigation, Layout │ │
│ │ (pre-rendered at build time) │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ 🕐 Dynamic │ │ 🕐 Dynamic │ │
│ │ User Info │ │ Recommendations │ │
│ │ (streamed) │ │ (streamed) │ │
│ └──────────────┘ └─────────────────────┘ │
└──────────────────────────────────────────────┘
// app/store/page.tsx — PPR in action
import { Suspense } from 'react';
import { cookies } from 'next/headers';
// Static part — rendered at build time
function ProductCatalog() {
return <StaticProductGrid />;
}
// Dynamic part — streamed at request time
async function PersonalizedRecommendations() {
const session = (await cookies()).get('session');
const recs = await getRecommendations(session?.value);
return <RecommendationCarousel items={recs} />;
}
export default function StorePage() {
return (
<div>
<ProductCatalog />
<Suspense fallback={<RecSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</div>
);
}
PPR across versions {PPR qua các phiên bản}:
- v14: Experimental flag
experimental.ppr = true{Cờ thử nghiệm} - v15: Stable, opt-in per route {Ổn định, bật từng route}
- v16: Default rendering strategy — PPR replaces the SSG/SSR binary choice {Chiến lược render mặc định — PPR thay thế lựa chọn nhị phân SSG/SSR}
Rendering Decision Flowchart {Sơ đồ quyết định Render}
Does your page use dynamic functions?
(cookies, headers, searchParams, uncached fetch)
YES → Does it need the fastest TTFB?
│
├── YES → PPR (static shell + dynamic holes)
│
└── NO → SSR (full dynamic render)
NO → Does data change periodically?
│
├── YES → ISR (static + background revalidation)
│
└── NO → SSG (pure static, rebuild to update)
Caching — The Biggest Evolution {Caching — Sự thay đổi lớn nhất}
Caching is what changed most dramatically across versions {Caching là thứ thay đổi nhiều nhất qua các phiên bản}. Understanding this evolution is critical {Hiểu sự thay đổi này là rất quan trọng}.
v14: Cache Everything by Default {v14: Cache mọi thứ mặc định}
In Next.js 14, the philosophy was aggressive caching {Ở Next.js 14, triết lý là cache tích cực}:
// Next.js 14 — This fetch is CACHED by default!
const data = await fetch('https://api.example.com/posts');
// Equivalent to: fetch(..., { cache: 'force-cache' })
// To opt OUT of caching, you had to be explicit:
const freshData = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});
Four caching layers in v14 {Bốn tầng cache ở v14}:
| Layer | What it caches {Cache cái gì} | Duration {Thời gian} |
|---|---|---|
| Request Memoization | Duplicate fetch() calls in same render {Các lệnh fetch() trùng trong cùng render} | Per-request {Mỗi request} |
| Data Cache | fetch() responses on server {Response fetch() trên server} | Persistent (until revalidated) {Vĩnh viễn (đến khi revalidate)} |
| Full Route Cache | Entire rendered HTML + RSC payload {Toàn bộ HTML + RSC payload đã render} | Persistent (static routes) |
| Router Cache | RSC payload on client for navigation {RSC payload trên client để navigate} | Session-based {Theo session} |
The problem {Vấn đề}: developers were confused {dev bị bối rối}. Data was stale and they didn’t know why {Data cũ mà không biết tại sao}. The implicit caching made debugging a nightmare {Caching ẩn khiến debug như ác mộng}.
v15: Cache Nothing by Default {v15: Không cache gì mặc định}
Next.js 15 reversed the default {Next.js 15 đảo ngược mặc định}:
// Next.js 15 — This fetch is NOT cached by default!
const data = await fetch('https://api.example.com/posts');
// Equivalent to: fetch(..., { cache: 'no-store' })
// To opt IN to caching:
const cachedData = await fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
// Or with revalidation:
const isrData = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
});
This was a breaking change {Đây là breaking change}. Apps that relied on implicit caching suddenly became slower {App dựa vào cache ẩn bỗng chậm đi} because every request hit the origin server {vì mọi request đều đến server gốc}.
v16: Explicit Caching with use cache {v16: Cache có chủ đích với use cache}
Next.js 16 introduces Cache Components {Next.js 16 giới thiệu Cache Component} — a completely new mental model {một mô hình tư duy hoàn toàn mới}:
// next.config.ts — Enable Cache Components
import type { NextConfig } from 'next';
const config: NextConfig = {
cacheComponents: true,
};
export default config;
File-level caching {Cache cấp file}
// app/products/page.tsx
'use cache';
// The ENTIRE page output is cached
export default async function ProductsPage() {
const products = await db.query('SELECT * FROM products');
return <ProductGrid products={products} />;
}
Function-level caching {Cache cấp hàm}
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache';
export async function getProducts() {
'use cache';
cacheLife('hours');
cacheTag('products');
return await db.query('SELECT * FROM products');
}
export async function getUser(id: string) {
'use cache';
cacheLife('minutes');
cacheTag(`user-${id}`);
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
}
Component-level caching {Cache cấp component}
// components/ProductCard.tsx
async function ProductCard({ id }: { id: string }) {
'use cache';
cacheTag(`product-${id}`);
const product = await getProduct(id);
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
Cache Profiles {Hồ sơ cache}
import { cacheLife } from 'next/cache';
// Built-in profiles:
cacheLife('seconds'); // short TTL
cacheLife('minutes'); // medium TTL
cacheLife('hours'); // long TTL
cacheLife('days'); // very long TTL
cacheLife('weeks'); // rarely changes
cacheLife('max'); // cache as long as possible
// Custom profile:
cacheLife({ stale: 300, revalidate: 60, expire: 3600 });
Invalidation in v16 {Invalidation ở v16}
import { revalidateTag } from 'next/cache';
// v16: revalidateTag now uses SWR pattern
// Serves stale while revalidating in background
await revalidateTag('products');
// New: updateTag() for immediate invalidation
import { updateTag } from 'next/cache';
await updateTag('products'); // Immediately purges
Caching Comparison Table {Bảng so sánh Caching}
| Behavior {Hành vi} | v14 | v15 | v16 |
|---|---|---|---|
fetch() default | Cached {Có cache} | Not cached {Không cache} | Not cached (use 'use cache' to opt in) |
| Opt-in mechanism | cache: 'no-store' to disable | cache: 'force-cache' to enable | 'use cache' directive |
| Granularity {Độ chi tiết} | Per-fetch | Per-fetch | File / Function / Component |
| Cache profiles | N/A | N/A | cacheLife() |
| Tag-based invalidation | revalidateTag() | revalidateTag() | revalidateTag() (SWR) + updateTag() |
| Mental model {Mô hình tư duy} | “Everything cached unless you say no” {Mọi thứ cache trừ khi nói không} | “Nothing cached unless you say yes” {Không gì cache trừ khi nói có} | “Mark what to cache, control how long” {Đánh dấu cái cần cache, kiểm soát bao lâu} |
Version Comparison {So sánh phiên bản}
The Big Picture {Bức tranh toàn cảnh}
| Feature {Tính năng} | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
| React version | React 18 | React 19 | React 19.2 |
| Bundler | Webpack (Turbopack opt-in) | Webpack (Turbopack stable for dev) | Turbopack default (dev + prod) |
| Caching default | Aggressive (cached) {Tích cực} | Opt-in (not cached) {Chủ động} | Explicit (use cache) {Rõ ràng} |
| Request APIs | Synchronous | Async (with deprecation warnings) {Bất đồng bộ (có cảnh báo)} | Strictly async {Bắt buộc bất đồng bộ} |
| Middleware | middleware.ts (Edge) | middleware.ts (Edge) | proxy.ts (Node.js) |
| PPR | Experimental | Stable (opt-in) | Default strategy {Mặc định} |
| Server Actions | Stable | Enhanced with validation | Mature {Trưởng thành} |
| React Compiler | N/A | Experimental | Stable (opt-in) |
| Node.js minimum | 18.17 | 18.18 | 20.9 |
next lint | Available | Available | Removed {Đã xoá} |
| AMP support | Deprecated {Không khuyến khích} | Deprecated | Removed {Đã xoá} |
| Build speed | Baseline | 2-3x faster (Turbopack dev) | 5-10x faster (Turbopack prod) |
Breaking Changes Per Version {Breaking Change mỗi phiên bản}
v14 → v15 Breaking Changes {Thay đổi gây lỗi từ v14 sang v15}
- Caching reversed {Caching đảo ngược}:
fetch()no longer cached by default {fetch()không còn cache mặc định} - Async Request APIs {API Request bất đồng bộ}:
cookies(),headers(),params,searchParamsbecame Promises {trở thành Promise} - React 19 {React 19}: new hooks, changed behavior for
refforwarding
// v14 → v15 migration for cookies
// Before (v14) — synchronous
import { cookies } from 'next/headers';
const token = cookies().get('session');
// After (v15) — asynchronous
import { cookies } from 'next/headers';
const token = (await cookies()).get('session');
// v14 → v15 migration for params
// Before (v14)
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}
// After (v15)
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}
v15 → v16 Breaking Changes {Thay đổi gây lỗi từ v15 sang v16}
- Turbopack default {Turbopack mặc định}: custom Webpack configs need
--webpackflag {config Webpack tuỳ chỉnh cần cờ--webpack} middleware.ts→proxy.ts{Đổi tên}: different filename AND function name {khác tên file VÀ tên hàm}next lintremoved {Đã xoá}: use ESLint or Biome directly {dùng ESLint hoặc Biome trực tiếp}- AMP removed {Đã xoá}: all AMP APIs and config gone {tất cả AMP API và config bị xoá}
- Node.js 18 dropped {Bỏ Node 18}: minimum is 20.9 {tối thiểu là 20.9}
- Async params strictly enforced {Params bất đồng bộ bắt buộc}: no more deprecation warnings, just errors {không còn cảnh báo, chỉ có lỗi}
revalidateTag()requires cache profile {revalidateTag()yêu cầu cache profile}publicRuntimeConfig/serverRuntimeConfigremoved {Đã xoá}: use env variables {dùng biến môi trường}
// v15 → v16 migration for middleware
// Before: middleware.ts
export function middleware(request: NextRequest) { /* ... */ }
// After: proxy.ts
export function proxy(request: NextRequest) { /* ... */ }
// v15 → v16 migration for caching
// Before (v15): per-fetch caching
const data = await fetch(url, {
cache: 'force-cache',
next: { revalidate: 3600, tags: ['products'] },
});
// After (v16): use cache directive
async function getProducts() {
'use cache';
cacheLife('hours');
cacheTag('products');
const res = await fetch(url);
return res.json();
}
Server Actions {Server Actions}
Server Actions let you run server code from client components {Server Actions cho phép bạn chạy code server từ client component} — like calling an API without building an API {như gọi API mà không cần xây API}:
// app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.insert({ title, content });
revalidateTag('posts');
}
// app/new-post/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Publish</button>
</form>
);
}
Evolution across versions {Phát triển qua các phiên bản}:
- v14: Stable, basic form handling {Ổn định, xử lý form cơ bản}
- v15: Enhanced with
useActionState, better error handling {Cải thiện vớiuseActionState, xử lý lỗi tốt hơn} - v16: Mature, integrates with
use cachefor optimistic updates {Trưởng thành, tích hợp vớiuse cachecho optimistic update}
Migration Cheat Sheet {Bảng tóm tắt di chuyển}
From Pages Router to App Router {Từ Pages Router sang App Router}
| Pages Router | App Router Equivalent |
|---|---|
pages/index.tsx | app/page.tsx |
pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
pages/_app.tsx | app/layout.tsx |
pages/_document.tsx | app/layout.tsx (html, body) |
pages/api/hello.ts | app/api/hello/route.ts |
getStaticProps | Just fetch in Server Component {Chỉ cần fetch trong Server Component} |
getStaticPaths | generateStaticParams() |
getServerSideProps | Fetch with cookies()/headers() or dynamic = 'force-dynamic' |
useRouter() (next/router) | useRouter() (next/navigation) + usePathname() + useSearchParams() |
Quick Upgrade Commands {Lệnh nâng cấp nhanh}
# Upgrade to v15 from v14
npx @next/codemod@latest upgrade 15
# Upgrade to v16 from v15
npx @next/codemod@canary upgrade latest
Conclusion {Kết luận}
The journey from Pages Router to App Router (v16) {Hành trình từ Pages Router đến App Router (v16)} represents a fundamental shift in how we think about React applications {đại diện cho sự thay đổi căn bản trong cách chúng ta nghĩ về ứng dụng React}:
- Routing {Routing}: from flat files to nested layouts with parallel routes {từ file phẳng đến layout lồng nhau với route song song}
- Rendering {Rendering}: from “pick one strategy per page” to “mix strategies within a single page” (PPR) {từ “chọn một chiến lược mỗi trang” sang “kết hợp chiến lược trong một trang” (PPR)}
- Caching {Caching}: from “trust the framework” to “be explicit about what you want” {từ “tin framework” sang “nói rõ bạn muốn gì”}
- Data {Data}: from special functions to just…
asynccomponents {từ các hàm đặc biệt sang chỉ… componentasync}
If you’re starting fresh in 2026 {Nếu bạn bắt đầu mới năm 2026}: use Next.js 16 with the App Router {dùng Next.js 16 với App Router}. The Pages Router still works but receives no new features {Pages Router vẫn hoạt động nhưng không có tính năng mới}.
If you’re migrating {Nếu bạn đang di chuyển}: go v14 → v15 → v16 incrementally {đi từng bước v14 → v15 → v16}. Each version’s codemod handles most of the work {Codemod mỗi phiên bản xử lý hầu hết công việc}. The App Router and Pages Router can coexist in the same project {App Router và Pages Router có thể cùng tồn tại trong một project}.
Resources {Tài liệu tham khảo}
- Next.js Documentation — Official docs, always up to date {Tài liệu chính thức}
- Next.js Upgrade Guide — Step-by-step migration {Hướng dẫn nâng cấp từng bước}
- React Server Components — Understanding RSC from React docs {Hiểu RSC từ tài liệu React}
- Vercel Blog: Rendering Strategies — Choosing the right strategy {Chọn chiến lược phù hợp}