CI 是綠的。
建置通過了。沒有 TypeScript 錯誤。沒有警告。看起來一切都很乾淨。我按下部署,然後去泡茶。
回來後打開 staging,結果有些地方壞掉了,而且壞得毫無道理。某個重新導向沒作用。lint 在建置流程裡悄悄消失了。某條 API 路由在第一次真正的請求時就拋錯。我兩週前寫的一個 revalidation 呼叫有在執行,卻什麼都沒做。
這些問題沒有一個在建置時出現。所有東西在出事前,看起來都完全正常。
這就是我在升級到 Next.js 16 時實際發生的事,以及你在上線前應該檢查什麼。
middleware.ts 停止運作,而且什麼都沒告訴我我的 middleware 檔案本身沒問題。它能編譯。匯出也合法。TypeScript 也沒意見。
升級到 Next.js 16 之後,它就不再對請求生效了。沒有錯誤。沒有棄用警告。終端機也沒有任何異常訊息。這個檔案就這樣被直接忽略了。
原因是:Next.js 16 把 middleware.ts 改成了 proxy.ts。專案位置一樣,但檔名不同。匯出的函式名稱也不同。
// 之前:middleware.ts
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// 之後:proxy.ts
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
改動就只有這樣:改檔名、改函式名。但因為舊檔案沒有丟出任何錯誤,我還以為它仍然有在跑。直到我預期中的重新導向沒有發生,才花了超久時間去查錯方向。
有一點要注意:如果你特別需要 edge runtime 的行為,middleware.ts 仍然保留給那個用途。在我的情況裡,升級後那段邏輯就不再執行了。把檔名和匯出名稱改掉後立刻就修好了。codemod 會自動處理這件事。但如果你是手動升級套件卻沒跑它,或它漏掉了某個檔案,這個問題會完全隱形。
上線前請先做:改檔名、改匯出名稱,並測試一個依賴它的路由。
revalidateTag('products') 能編譯、能部署,卻默默做了錯的事在遷移過程中我寫了這樣的程式:
revalidateTag('products')
只有一個參數。在 Next.js 15 裡這很正常。我兩週前寫過一次,之後就沒再想它。
在 Next.js 16 裡,單一參數的形式已被棄用,而且會產生 TypeScript 錯誤。但前提是你的 tsconfig 有開 strict mode。我的沒有。那是很多年前從舊專案搬來的設定,之後就沒動過。
所以它編譯過了。部署成功了。也真的執行了。只是它回退到舊的 invalidation 行為,而不是新的 SWR 型系統。頁面沒有反映 mutation。沒有任何錯誤,只有一直是舊資料,而且我比應該的時間更久才把原因歸到別的地方。
修正方式只是加上第二個參數:
revalidateTag('products', 'max') // SWR,建議預設
revalidateTag('products', { expire: 0 }) // 立即過期,適合 webhook
codemod(npx @next/codemod@canary upgrade latest)會處理這件事。但如果你升級後又寫了新的 revalidation 呼叫,或 codemod 漏掉了某個檔案,請手動檢查。
真正的修正是把 tsconfig 的 strict mode 打開。這個改動會讓它變成編譯錯誤,而不是悄悄在 runtime 出問題:
{
"compilerOptions": {
"strict": true
}
}
先做這件事。
next lint 不見了,而我的 CI 還一直顯示通過這件事聽起來不大,但其實不是。
next lint 在 Next.js 16 裡被完整移除了。不是棄用,不是改名,就是沒了。next.config.ts 裡的 eslint 選項也被移除了。next build 也不再自動執行 lint。
我的 CI 設定成把 next lint 當作其中一個步驟。升級之後,那個指令已經不存在了。依照你的 CI 如何處理缺失的指令,它可能會明確失敗,也可能會安靜地成功然後繼續往下跑。我的就是直接往下跑。
所以我等於是在沒有 lint 的情況下送出程式碼,而 CI 還回報綠燈。直到有個明顯問題漏過去,而我原本以為 lint 會抓到它,我才發現這件事。
遷移方式是直接執行 ESLint:
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
codemod 會建立 eslint.config.mjs,並更新你 package.json 的 scripts。但你的 CI 設定是另一個檔案,codemod 不會碰它。兩邊都要檢查。
paramscodemod 幫我正確更新了大多數頁面。但我有一個 layout 檔案沒被處理到。那個元件直接讀取 params,沒有先 await,在 Next.js 15 這樣還可以,但在 16 就不對了,因為現在的 params 是 Promise。
// 之前 — Next.js 15
export default function Layout({ params }: { params: { id: string } }) {
const id = params.id
}
// 之後 — Next.js 16
export default async function Layout({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
}
這個問題確實有拋錯,但只有在 staging 中該路由第一次真正收到請求時才會發生,不會在建置時出現。建置是完全乾淨地通過。
如果你有 layouts、pages 或 route handlers,請整個 codebase 搜尋直接使用 params. 的地方,確認每一個都已更新。同樣也要檢查 searchParams、cookies()、headers()、draftMode()。現在都變成非同步了,都需要 await。
這些都不是快取 bug。它們是升級 bug。這類問題的特徵就是:建置會通過、程式碼在技術上是合法的,但錯誤行為只會在特定條件下出現:真正觸發重新導向、需要反映 mutation、lint 問題進到 review、某條特定路由被打到。
codemod 可以處理大部分內容。在你改其他東西之前,先跑 npx @next/codemod@canary upgrade latest。然後手動檢查三件事:搜尋所有只有一個參數的 revalidateTag(、檢查 CI 設定裡是否還有 next lint、以及打開 strict TypeScript。這三項可以涵蓋 codemod 最容易漏掉的大部分內容。
如果你已經完成升級,現在正在處理的是快取行為本身,那麼這系列前面的文章有涵蓋這部分。我做的除錯器,讓開發期間的快取行為變得可視化 以及 那七個在 production 中編譯正常、卻會悄悄壞掉的 bug。
如果你想要完整參考,我也有一份包含前後對照的完整逐步遷移指南:shubhra.dev/tutorials/nextjs-16-cache-components
你遇到的是哪一個?還是還有我這裡沒提到的問題?
原文出處:https://dev.to/shubhradev/nextjs-16-broke-my-app-in-4-places-and-none-of-them-threw-an-error-51mn