skip to content
BlogZzz

[Web] Next.js - Pages Router 預取行為異常

/ 6 min read

Table of Contents

前言

有一個專案是用 Next.js Pages router 開發的

之前遇到連結後點進去,有時頁面會載入失敗,Network 裡看到 _next/data/...json 是 {}

追查了一陣子,最後發現是 Next.js Prefetch 的問題


問題現象

在正式環境中,觀察到以下現象:

  1. Hover 連結後點擊,頁面載入失敗,即使設定 prefetch={false}
  2. Network 看到 /_next/data/...json 內容不是預期的網頁資料,而是 {}

像是「prefetch 快取到壞掉的 JSON」,真正點擊時拿到錯誤資料,頁面就炸了


背景知識:Next.js Prefetch 是什麼?

當使用者在頁面上看到連結時,Next.js 會在背景預先抓取目標頁面的 JSON 資料

使用者真正點擊時,頁面可以瞬間渲染,大幅提升使用者體驗

Prefetch 的觸發時機

  • 預取會在 Link 元件進入使用者視窗時發生,初始載入或透過滾動

Next.js 會在背景中預先載入連結路線(由 href 標示)以及相關資料,以提升客戶端導航的效能

預取僅在生產環境中啟用

// 預設會 prefetch
<Link href="/videos/123">看影片</Link>
// 關閉 prefetch
<Link href="/videos/123" prefetch={false}>看影片</Link>

prefetch 屬性的值:

  • true(預設值):完整路徑及其資料將會預取
  • false:理論上不預取,但實測 hover 仍可能觸發

如果想完全移除 hover 時的預取,可以用 a 標籤,或逐步採用 App Router,因為可以在 hover 時關閉預取

Prefetch 專用的 Header

當 Next.js 發起 prefetch 請求時,會帶上兩個關鍵 header:

  • purpose: prefetch — 告訴伺服器這是預取請求
  • x-middleware-prefetch — Next.js 內部用來標記這是 prefetch 的 header

伺服器可以根據這些 header 來決定是否要跑完整的 SSR 流程,或是返回簡化的預覽資料


問題根因分析

從官方 issue 內容來看,最可能原因是 Next.js 某些版本在「Middleware + /_next/data」流程有 bug,導致部分 data request 回傳空的 {},並帶上 x-middleware-next: 1 / x-middleware-skip

這通常會被下列情況放大:

  • 預取與正式導頁共用同一個 /_next/data/* URL,預取回應可能被標記為 skip 或空內容
  • 反向代理或 CDN(例如 Nginx cache)把「預取或 skip 的空 JSON」快取起來,真正導頁就拿到 {}
  • Middleware 有 set-cookie 或自訂 header,觸發了 Next 內部「跳過或重用 data response」的最佳化路徑
  • middleware 中有 fetch 或 rewrites,可能讓資料流在某些情況下被提早中止(更少見,但也會出現空 body

解法:讓 Prefetch 繞過特殊處理

在 Middleware 偵測到 prefetch 請求時,主動刪除 x-middleware-prefetch header,讓它不要走 Next.js 的特殊 prefetch 路徑,而是走正常的 SSR 流程

function handlePrefetch(request: NextRequest): Headers {
const reqHeaders = new Headers(request.headers);
const purpose = reqHeaders.get("purpose");
const isPrefetch = purpose && purpose.match(/prefetch/i);
if (isPrefetch) {
// 刪除這個 header,讓 prefetch 走正常流程
reqHeaders.delete("x-middleware-prefetch");
}
return reqHeaders;
}

這個做法的好處是:

  • Prefetch 請求會得到完整的 SSR 結果
  • 不會拿到錯誤的快取 JSON
  • 正式環境的穩定性大幅提升

代價與效能取捨

任何技術決策都有代價,這個修法也不例外

優點

  1. 穩定性提升:不再出現 hover 後頁面壞掉的問題
  2. 使用者體驗改善:點擊後頁面一定能正確載入
  3. Debug 更容易:不再需要處理奇怪的 prefetch 快取問題

代價

  1. Prefetch 效率降低:因為走正常流程,prefetch 成本變高
  2. 伺服器負擔增加:每次 prefetch 都會觸發完整 SSR
  3. 首點延遲可能增加:如果完全關閉 prefetch,首次點擊會稍微慢一點

進階方案:如果還是想保留 Prefetch 效能

如果你的網站對效能要求更高,或 prefetch 問題讓你很苦惱,可以考慮以下方案

直接關閉 prefetch:

import { useRouter } from "next/router";
import { useEffect } from "react";
const useConditionalPrefetch = () => {
const router = useRouter();
useEffect(() => {
// 直接把 prefetch 變成空操作,等同關閉
router.prefetch = () => Promise.resolve();
}, [router]);
};

然後在 _app.tsx 中使用


如何驗證問題是否解決

如果你也遇到類似問題,可以用以下方式驗證

1. 打開 DevTools Network

  • Hover 一個連結
  • 觀察 /_next/data/...json 的內容

2. 使用 Production Build 測試

Terminal window
npm run build
npm start
# 然後用正式環境的行為測試

總結

這次的踩坑經驗 穩定性優先於效能 寧可稍微慢一點,也不要使用者看到錯誤


參考資源