Nâng cấp Next.js 15 lên 16 — Turbopack, static export và ba chỗ vấp
Ghi chép thật từ đợt nâng cấp site này lên Next.js 16 — vì sao vẫn giữ static export cho Nginx, Turbopack khắt khe hơn webpack ở đâu, và ba lỗi ngốn nhiều giờ nhất.
Site này chạy Next.js Pages Router, nội dung viết bằng Markdown, build ra HTML tĩnh rồi để Nginx phục vụ thẳng thư mục out/. Không có Node server nào chạy nền. Đợt vừa rồi tôi đưa nó từ Next.js 15 lên Next.js 16 — bản có Turbopack làm bundler mặc định cho cả dev lẫn build.
Bài này ghi lại những gì thật sự tốn thời gian, không phải changelog. Nếu bạn đang cân nhắc nâng cấp một site tĩnh tương tự, ba mục dưới đây là chỗ nên đọc kỹ.
Vì sao vẫn giữ static export thay vì đổi sang server
Cám dỗ lớn nhất khi nâng version là "nhân tiện" đổi luôn kiến trúc: App Router, server components, ISR. Tôi bỏ qua tất cả, giữ nguyên output: 'export'.
Lý do nằm ở chỗ deploy: server hiện tại chỉ có Nginx trỏ vào một thư mục. Không PM2, không process cần restart, không cổng cần mở. Deploy = build ở máy local, commit thư mục out/, rồi git pull trên server. Muốn rollback thì git revert. Toàn bộ mô hình vận hành đó biến mất ngay khi cần một Node process chạy nền.
Đổi lại, tôi mất khả năng dùng ISR và middleware. Với một site nội dung tĩnh, đó là cái giá rẻ.
Một thay đổi cần biết: lệnh next export không còn tồn tại. Từ Next 13 trở đi cấu hình output: 'export' trong next.config.js đảm nhiệm việc này, và next build tự sinh ra out/. Script nào còn gọi next export sẽ fail ngay.
Turbopack khắt khe hơn webpack ở chỗ nào
Turbopack là bundler mặc định của Next 16. Nó nhanh hơn rõ rệt — dev server khởi động dưới một giây — nhưng nghiêm ngặt hơn webpack ở phần CSS. Hai lỗi dưới đây làm tôi mất buổi sáng.
@import phải đứng trước mọi rule khác
File globals.css của tôi có dòng @import 'prism.css' nằm giữa file, sau vài rule. Webpack im lặng cho qua. Turbopack fail build ngay, và nó đúng: CSS spec quy định @import phải đứng đầu.
/* Đúng: @import trước tiên, rồi mới tới phần còn lại */
@import 'prism.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
Sửa mất ba giây. Tìm ra thì lâu hơn, vì thông báo lỗi không chỉ đúng dòng.
Comment CSS nuốt mất selector
Lỗi này mới thật sự khó chịu. Tailwind báo Unexpected '/' — không kèm tên file, không số dòng. Build fail, còn dev server thì chạy bình thường.
Thủ phạm là một comment tôi viết để giải thích hệ màu:
/* Component dùng class bg-omega-*/text-omega-* sẽ tự đổi theo theme */
Chuỗi */ nằm giữa câu đóng comment sớm. Phần còn lại (text-omega-* sẽ tự đổi theo theme */) rơi ra ngoài, trở thành selector rác. Tôi phải bisect từng file CSS bằng Tailwind CLI mới khoanh được vùng.
Bài học giữ lại: đừng bao giờ viết chuỗi */ bên trong comment CSS — kể cả khi đang mô tả tên class có dấu sao.
Light mode không cần viết dark: cho từng class
Phần này không liên quan tới Next 16, nhưng là thay đổi đáng giá nhất trong đợt làm.
Cách thông thường là mỗi class màu viết một cặp: bg-zinc-900 dark:bg-zinc-50. Với vài trăm class thì việc sửa theme sau này thành cực hình. Thay vào đó, tôi để toàn bộ thang màu trung tính chạy qua CSS variables, và light mode chỉ là một khối đảo ngược thang đó:
html:not(.dark) {
--color-omega-50: 24 24 27; /* zinc-900 */
--color-omega-100: 39 39 42; /* zinc-800 */
--color-omega-800: 244 244 245; /* zinc-100 */
--color-omega-900: 250 250 250; /* zinc-50 */
}
Mọi component viết bg-omega-900 hay text-omega-400 tự đảo màu khi đổi theme, không sửa một dòng JSX nào. Chỉ những chỗ hardcode (bg-black, text-white) mới cần cặp dark: — và đó cũng là danh sách ngắn đáng để rà lại.
Static export vẫn làm được nhiều hơn bạn nghĩ
Bỏ server không có nghĩa là bỏ tính năng. Ba thứ tôi vẫn giữ:
Command palette ⌘K — dữ liệu tìm kiếm là một file JSON sinh lúc build. Một script Node quét frontmatter toàn bộ nội dung, ghi ra public/search-index.json, client chỉ việc fetch. Thêm bài mới không phải đụng vào code.
node scripts/generate-search-index.js
# search-index: 15 entries -> public/search-index.json
Form liên hệ — gọi thẳng Web3Forms từ trình duyệt, không cần API route.
RSS và sitemap — sinh ở bước postbuild, ghi thẳng vào out/.
Điểm chung: mọi thứ đẩy về lúc build hoặc về phía client. Cái gì bắt buộc phải có server thì cân nhắc xem có thật sự cần không — phần lớn là không.
Checklist nếu bạn sắp nâng cấp
Rút ra sau khi làm thật, theo thứ tự nên chạy:
- Node ≥ 20.9 — Next 16 yêu cầu, và vài dependency sẽ chặn cứng bằng trường
engines. - Bỏ
next exportkhỏi mọi npm script, chuyển sangoutput: 'export'. - Rà file CSS:
@importlên đầu, xoá mọi chuỗi*/nằm trong comment. next lintkhông còn — Next 16 gỡ lệnh này, gọi thẳngeslint .và chuyển sang flat config.- Kiểm tra
next/image: proponLoadingCompleteđã bị gỡ, dùngonLoad. - Kiểm tra
next/link:legacyBehaviorvàpassHrefcũng không còn. - Xoá
.next/trước lần chạy đầu — cache Turbopack cũ hay gây lỗi module lạ.
Điều bất ngờ nhất: phần khó không nằm ở API mới của Next, mà ở CSS. Nếu dự án của bạn có file style tích tụ nhiều năm, hãy dành thời gian cho nó trước.
Toàn bộ site này là mã nguồn mở về mặt cách làm — nếu bạn muốn xem kết quả, danh sách dự án và trang dịch vụ đều chạy trên đúng pipeline vừa mô tả.
Bài viết liên quan