Nâng cấp lên Next.js 16: những gì thật sự làm build dừng
Tài liệu Next.js nói cái gì đã đổi. Trang này nói cái gì xảy ra khi bạn chạy: một ứng dụng thật trên next@14.2.35 được nâng lên next@16.4.3, build bốn lần, và mỗi lần đều ghi lại lỗi nguyên văn, exit code và kết quả đo trên server đã build. Mọi con số bên dưới đến từ lần chạy đó, không phải từ ký ức.
Mã trên trang này là một ứng dụng chạy được: 22/22 kiểm tra đạt ngày 02/11/2026. Phiên bản đã chạy thử: next@16.4.3, react@19.3.1, typescript@7.0.2, Node.js v26.0.0.
Chọn bản đích trước khi sửa gì
Trên npm hôm 2/11/2026, next có nhãn latest là 16.4.4; dòng 14 dừng ở 14.2.35, dòng 15 ở 15.5.28. Trang này bắt đầu từ 14.2.35 rồi đi thẳng lên 16.4.3, vì đó là bản mà nhiều dự án còn chạy. Nếu bạn đang ở 15, các bước giống hệt, chỉ bỏ phần React 19.
Yêu cầu mới: Node.js 20.9 trở lên (Node 18 không còn được hỗ trợ), TypeScript 5.1 trở lên, Chrome/Edge/Firefox 111+ và Safari 16.4+.
Thứ tự chạy: codemod trước, đọc từng thay đổi sau
Codemod trong tài liệu Next.js: npx @next/codemod@canary upgrade latest. Theo tài liệu, nó chuyển next.config sang khoá turbopack mới, đổi next lint sang ESLint CLI, đổi middleware sang proxy, bỏ tiền tố unstable_, và bỏ experimental_ppr.
Đừng coi codemod là xong việc. Chạy npx next typegen để sinh kiểu PageProps, LayoutProps, RouteContext (có từ 15.5), rồi để trình biên dịch chỉ từng chỗ còn đọc params đồng bộ. Trong ứng dụng mẫu, build vẫn xanh dù chỗ đọc đồng bộ còn đó, nên bước rà tay là bắt buộc.
Turbopack mặc định: cấu hình webpack làm build dừng
Next.js 16 dùng Turbopack cho cả next dev và next build; cờ --turbopack không cần nữa. Nếu next.config còn khoá webpack, build dừng: đo được exit 1 và dòng custom webpack configurations may need to be migrated to Turbopack.
Ba cách theo tài liệu: chuyển cấu hình sang khoá turbopack ở cấp cao nhất; bỏ cấu hình nếu không cần; hoặc giữ webpack bằng next build --webpack. Đo thêm: truyền rõ --turbopack hoặc --webpack cũng làm hết lỗi đó, vì bạn đã chọn tường minh.
Thông báo lỗi nhắc một chỗ dễ mất thời gian: cấu hình webpack có thể do plugin thêm vào, không phải bạn viết. Khi đó tìm trong next.config và trong từng plugin, đừng chỉ đọc file cấu hình.
params và searchParams: await — và build xanh không cứu bạn
Đây là thay đổi chạm nhiều file nhất. Next.js 15 cho phép đọc đồng bộ tạm thời; 16 bỏ hẳn. params (page, layout, route handler, default, ảnh metadata), searchParams, cookies(), headers(), draftMode() đều là Promise.
Điều bất ngờ, đo được: next@16.4.3 **không** báo lỗi cho trang còn đọc searchParams đồng bộ. Build xanh, exit 0, và trang được prerender thành tĩnh (○ /) vì lúc build searchParams là object rỗng. Trên server đã build, GET /?q=banh trả 200 nhưng chữ “banh” không có trong HTML: tham số truy vấn lặng lẽ mất tác dụng. Không lỗi, không cảnh báo.
Vì thế đừng chờ compiler bắt lỗi. Chạy npx next typegen để có PageProps, LayoutProps, RouteContext, rồi tự rà mọi chỗ còn searchParams.x, params.y, cookies(), headers(), draftMode() mà không await — và mở từng route thật, có tham số, để so với trước khi nâng cấp.
Cách viết gọn với TypeScript: export default async function Page(props: PageProps<'/blog/[slug]'>) rồi const { slug } = await props.params. Không phải tự khai lại hình dạng của params.
import Image from "next/image";
// Next.js 14: searchParams is a plain object, read synchronously, and the type
// checker agrees with that. Removing that sync access is the biggest code
// change on the way to 16.
export default function Page({ searchParams }: { searchParams: { q?: string } }) {
return (
<main>
<h1>Cửa hàng</h1>
<p>Đang tìm: {searchParams.q ?? "chưa nhập"}</p>
<Image src="/anh.png" alt="Một điểm ảnh" width={64} height={64} quality={90} />
</main>
);
}import Image from "next/image";
import Link from "next/link";
// searchParams is a Promise in Next.js 16 — read it with await.
export default async function Page({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
const { q } = await searchParams;
return (
<main>
<h1>Cửa hàng</h1>
<p>Đang tìm: {q ?? "chưa nhập"}</p>
<p>
<Link href="/cua-hang/ao-thun">Áo thun</Link>
</p>
<Image src="/anh.png" alt="Một điểm ảnh" width={64} height={64} quality={90} />
<Image src="/anh.png" alt="Một điểm ảnh" width={64} height={64} quality={65} />
</main>
);
}// params is a Promise too, in pages, layouts, route handlers and metadata images.
export default async function Product({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return (
<main>
<h1>Sản phẩm</h1>
<p>Mã: {slug}</p>
</main>
);
}middleware.ts thành proxy.ts
File middleware đổi tên thành proxy, hàm xuất ra cũng đổi tên. Runtime là Node.js và không đổi được; edge chỉ còn dùng được với middleware.
Đo được hai trạng thái. Một: chỉ có middleware.ts trên 16.4.3 vẫn build, kèm cảnh báo The "middleware" file convention is deprecated. Please use "proxy" instead. và danh sách route vẫn ghi Proxy (Middleware). Hai: để cả hai file thì build dừng — Both middleware file "./middleware.ts" and proxy file "./proxy.ts" are detected. Nghĩa là đổi tên phải xoá file cũ, không được để song song.
Sau khi đổi, đo trên server đã build: GET /cu trả 307 /cua-hang, và mọi response có header x-proxy: 1 do proxy đặt. Các cờ cấu hình đổi tên theo, ví dụ skipMiddlewareUrlNormalize thành skipProxyUrlNormalize.
import { NextResponse, type NextRequest } from "next/server";
// Next.js 15: the file is middleware.ts and the export is middleware.
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("x-proxy", "1");
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};import { NextResponse, type NextRequest } from "next/server";
// Next.js 16: middleware.ts is now proxy.ts, and the function is named proxy.
// The runtime is Node.js and cannot be changed.
export function proxy(request: NextRequest) {
if (request.nextUrl.pathname === "/cu") {
return NextResponse.redirect(new URL("/cua-hang", request.url));
}
const response = NextResponse.next();
response.headers.set("x-proxy", "1");
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};next/image: bốn giá trị mặc định đổi
images.qualities mặc định chỉ còn [75]. Truyền quality không nằm trong danh sách thì Next.js kéo về giá trị gần nhất, không cảnh báo. Đo trên ứng dụng mẫu (danh sách [50, 75, 90, 100]): quality={90} ra q=90, quality={65} bị kéo về q=75.
images.minimumCacheTTL từ 60 giây lên 4 giờ. Đo trên ảnh đã tối ưu: cache-control: public, max-age=14400, must-revalidate.
Ba thay đổi nữa: 16 bị bỏ khỏi imageSizes mặc định; ảnh nội bộ có query string phải khai images.localPatterns.search; images.maximumRedirects mặc định còn 3. Và images.domains đã bị bỏ dần — đo được cảnh báo images.domains is deprecated in favor of images.remotePatterns, thay bằng remotePatterns.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
// Next.js 16 allows only quality 75 unless you list the others. 90 is the
// value this app renders with; 65 is deliberately missing, to show coercion.
qualities: [50, 75, 90, 100],
},
};
export default nextConfig;next lint đã bị bỏ
Lệnh next lint không còn trong Next.js 16. Đo được: chạy next lint trên 16.4.3 in Invalid project directory provided, no such directory: …/lint và thoát với mã 1 — Next coi lint là tên thư mục dự án.
next build cũng không chạy lint nữa, và khoá eslint trong next.config đã bị bỏ. Dùng ESLint hoặc Biome trực tiếp trong CI; codemod next-lint-to-eslint-cli chuyển cấu hình giúp.
Slot song song cần default.js
Tài liệu 16 yêu cầu mỗi slot song song có file default.js; ứng dụng mẫu thêm app/@modal/default.tsx trả null. Trong lần chạy này slot đó không có route con, nên đừng dựa vào lỗi build để phát hiện thiếu default.js — hãy rà danh sách slot trong app/ bằng mắt.
// Next.js 16: every parallel route slot needs an explicit default file, or the
// build fails. This slot has nothing to render until a child route fills it.
export default function ModalDefault() {
return null;
}Những thứ khác đã bị bỏ (chưa chạy ở đây)
Bị bỏ hẳn: AMP (amp trong config, next/amp), serverRuntimeConfig/publicRuntimeConfig (dùng biến môi trường), các tuỳ chọn devIndicators cũ, experimental.dynamicIO (đổi thành cacheComponents ở cấp cao nhất), unstable_rootParams (thành rootParams).
API cache đổi cách gọi: revalidateTag nhận thêm tham số profile, và có updateTag, refresh, cacheLife, cacheTag. App Router dùng React 19.2 (View Transitions, useEffectEvent, Activity), và React Compiler đã ổn định với reactCompiler: true.
Ứng dụng mẫu không dùng mục nào trong số này, nên trang chỉ nêu theo tài liệu, không nhận là đã chạy.
Thứ tự làm trong một buổi
1. npm install next@latest react@latest react-dom@latest, kèm @types/react và @types/react-dom mới. 2. npx @next/codemod@canary upgrade latest, đọc diff trước khi commit. 3. Bỏ webpack khỏi next.config, chuyển sang turbopack hoặc dùng --webpack. 4. npx next typegen, rồi thêm await ở params, searchParams, cookies, headers, draftMode. 5. Đổi middleware.ts thành proxy.ts — xoá file cũ. 6. Rà next/image: qualities, minimumCacheTTL, imageSizes, localPatterns. 7. Bỏ next lint khỏi script, chạy ESLint hoặc Biome trong CI. 8. next build xanh, rồi next start và mở từng route.
Bước 8 không được bỏ: build xanh chỉ chứng minh mã biên dịch được. Trong lần chạy này, trang còn đọc searchParams đồng bộ vẫn build, vẫn trả 200, nhưng tham số trên URL không còn tác dụng — chỉ mở route mới thấy.
Đã kiểm tra
- 1. next@14.2.35 builds the app as it was:
exit 0 - 2. the same code on 16 fails to build:
exit 1 - 2. the build says why: the custom webpack config:
custom webpack configurations may need to be migrated to Turbopack. - 3. with the webpack config gone the old code builds on 16:
exit 0 - 3. but the build names the file that was renamed:
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead. - 3. and the images.domains config:
⚠ `images.domains` is deprecated in favor of `images.remotePatterns`. Please update next.config.js to protect your application from malicious users. - 3. the page built this way answers:
200 - 3. the query string reaches it:
200 q=banh NOT rendered - 4. the upgraded app builds on 16.4.3:
exit 0 - 5. `next lint` was removed — next now reads `lint` as a project directory:
exit 1: Invalid project directory provided, no such directory: …/lint - /: 200 with the page:
200 - /: searchParams awaited, the default renders:
Đang tìm: chưa nhập - /: proxy.ts runs (x-proxy header set by the proxy):
1 - /: the route is dynamic because it reads searchParams:
private, no-cache, no-store, max-age=0, must-revalidate - /cua-hang?q=banh: the query string reaches the page:
Tìm: banh - /cua-hang/ao-thun: awaited params render the slug:
Mã: ao-thun - /cu: proxy.ts redirects the old path:
307 /cua-hang - /: quality={90} is served at 90 because images.qualities lists it:
90 - /: quality={65} is not listed, so it is coerced to the closest listed value (75):
75 - /_next/image: the optimized image is served:
200 - /_next/image: default minimumCacheTTL is 4 hours (14400 s):
max-age=14400 - /: page size before compression:
6536 bytes
Chạy lại kiểm tra: bun scripts/guides/verify-next-16.ts
Liên quan
- Đa ngôn ngữ (i18n) với Next.js: /vi và /en bằng next-intl
- Next.js là gì? Frontend hay backend, dùng khi nào
- Lỗ hổng bảo mật Next.js
- Kiểm tra phiên bản Next.js của trang bạn
Nguồn
- Next.js docs — Upgrading to Version 16
- Next.js docs — Codemods (the `upgrade` codemod)
- Next.js docs — proxy.js (middleware renamed in v16.0.0)
- Next.js docs — next/image
- Next.js docs — the next CLI (next lint removed, next typegen)
- Next.js 16 release notes
- React 19.2 release
- npm registry — next dist-tags
Tóm tắt và diễn giải bằng lời của chúng tôi; mã viết lại từ ví dụ trong tài liệu Next.js. Khi có khác biệt, tài liệu Next.js là chuẩn.