掌握 Next.js 載入順序是每位前端開發者在進入 Pages Router 架構時必經的基礎核心。在 Next.js 的生態系中,middleware.js、_app.js、_document.js 與 index.js 等特殊檔案各自在請求生命週期的不同階段扮演關鍵角色。理解這些檔案的執行先後順序與運作機制,能幫助你在正確的層級處理權限驗證、全域狀態、HTML 結構與頁面渲染,大幅提升應用程式的穩定度與架構可維護性。
⚡ 快速跳轉:
整體執行順序
Next.js 的執行順序與載入機制需要分兩種情境來看:
⚠️ 架構防雷提醒:本文探討的
_app.js與_document.js是 Next.js 傳統 Pages Router 架構(位於pages/目錄)的專屬核心檔案。如果你的專案是採用 Next.js 13+ 預設的 App Router 架構(位於app/目錄),這兩個檔案已被全新設計的layout.js與template.js取代,請務必確認專案目錄結構再進行設定。
1. 當用戶首次發出 HTTP 請求(例如訪問 https://example.com/)時的執行順序:
middleware.js:位於專案根目錄或src目錄,負責在請求抵達路由之前進行攔截、重新導向或修改headers。_app.js:負責管理全域狀態與共用 CSS 樣式,包裹所有渲染的頁面元件。_document.js:只在伺服器端執行,用來擴展與客製化 HTML 文件的基本結構(如<html>、<head>與<body>)。index.js:實際對應路由並渲染頁面內容的目標元件。

2. 客戶端路由切換(Client-Side Transition)時的執行順序:
當用戶已經在網站內,點擊連結切換到其他頁面時:
middleware.js不會執行(除非透過 Middleware 進行 API 路由攔截,一般頁面切換由前端 Router 處理)。_app.js仍然存在並持續運行(不會重新掛載,僅切換內部的Component)。_document.js不會重新載入(HTML 骨架已經在首次請求時建立)。- 目標頁面元件(如
index.js或其他頁面)會透過非同步載入並重新渲染。

3. 生命週期執行階段與行為對照表
| 檔案名稱 | 首次 SSR 請求執行狀態 | 客戶端路由切換執行狀態 | 主要職責與運行環境 |
|---|---|---|---|
middleware.js | 每次請求最先執行 | 預設不執行(除非觸發特定 API 路由) | 請求攔截、權限驗證、邊緣運算 (Edge) 重新導向 |
_app.js | 伺服器端與客戶端皆初始化 | 持續運行(不重新掛載,僅切換元件) | 全域 Layout 包裹、Context Provider、狀態維持 |
_document.js | 僅在伺服器端執行一次 | 不執行 | 客製化 HTML 骨架、<head> 屬性注入、SSR 結構 |
目標頁面 (index.js) | 伺服器端渲染後傳送至客戶端 | 客戶端非同步載入與渲染 | 頁面專屬 UI 與資料抓取 (Data Fetching) |
middleware.js 說明與實作
middleware.js 主要用來在請求完成前進行攔截與邏輯判斷,例如權限驗證、地理位置偵測或動態重新導向。

簡單解釋:
這就像是機場的安檢站,在乘客登機前(進入應用程式)先檢查護照與行李,決定是否讓乘客順利通關、轉搭其他航班(重新導向),或是直接拒絕訪問。
載入時機:
在每個 HTTP 請求進入應用程式時最先執行,並且在路由匹配與伺服器處理之前運行。
middleware.js 基本程式碼範例
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('token');
// 如果沒有 token 且嘗試訪問保護頁面,則重定向到登入頁
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// 設定只有特定路徑會觸發此 Middleware
export const config = {
matcher: ['/dashboard/:path*'],
};
_document.js 說明與實作
_document.js 用來擴展 Pages Router 中的 HTML 文件的基本結構,例如自訂 <html> 標籤的語系、載入外部字型或注入第三方指令碼。

簡單解釋:
想像它是一個 HTML 模板,讓你可以在所有頁面共用底層的標籤與結構。這就像是飛機的機身設計,決定了所有乘客座位與逃生出口的基礎框架,但不負責具體的航班資訊與旅客服務。
載入時機:
只在伺服器端渲染(SSR)時執行一次,用來生成完整的 HTML 基礎骨架,在瀏覽器端進行客戶端導航時不會重新執行。
_document.js 基本程式碼範例
import Document, { Html, Head, Main, NextScript } from 'next/document';
export default function MyDocument() {
return (
<Html lang="zh-Hant">
<Head>
{/* 可在此處載入全域字型或共用 Meta 標籤 */}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
_app.js 說明與實作
_app.js 是 Pages Router 中控制全域頁面初始化的最外層元件。所有頁面的切換與共用版面配置都會通過這裡。

簡單解釋:
這就像是登機閘口,確保每位乘客在進入登機橋前都經過必要的統一流程,例如登機證核對(全域狀態與權限控管)、行李統一規範(共用樣式與佈局包裝)。
載入時機:
在每個頁面加載時執行,非常適合用來放置共用的導覽列、Footer、狀態管理(如 Redux Provider)或錯誤邊界(Error Boundary)。
_app.js 基本程式碼範例
import '../styles/globals.css';
export default function MyApp({ Component, pageProps }) {
// 可以在這裡包裹全域 Layout、Context Provider 或 Redux Store
return (
<div className="app-container">
<Component {...pageProps} />
</div>
);
}
根目錄 index.js 說明
定義網站首頁的實際內容與檢視邏輯。

簡單解釋:
預設的首頁元件,如同你在瀏覽器網址列輸入 https://example.com/ 時,在沒有其他重定向設定的前提下,最終導向並渲染的目標頁面。
載入時機:
只有在用戶訪問根路徑 / 時才會執行,負責首頁專屬的資料抓取(如 getStaticProps 或 getServerSideProps)與 UI 渲染。
實務常見踩坑與排錯心法
在實務開發中,工程師在處理 Next.js Pages Router 的生命週期與這幾個核心檔案時,最常踩到以下幾大常見情境,以下整理對應的排錯案例與避坑心法:
1. 在 _document.js 裡面寫業務邏輯或綁定事件監聽(如 onClick)
- 踩坑情境:
初學者常誤以為_document.js是全域頁面的包裝器,因而嘗試在<body>或<Head>裡面加入useState、useEffect,或是綁定按鈕點擊事件。 - 為什麼會出錯:
_document.js只在伺服器端渲染 (SSR) 時執行,用來建立最外層的 HTML 骨架。當 HTML 傳送到客戶端進行 Hydration 之後,_document內部的程式碼完全不會被當成 React 元件重新運作。 - 排錯與解決方法:
症狀:主控台噴出window is not defined、事件完全沒有反應,或是水合錯誤(Hydration Mismatch)。
解法:任何與瀏覽器互動、狀態管理、UI 互動邏輯,一律必須放在_app.js或具體的頁面元件(如index.js)中。

2. 誤用 middleware.js 進行龐大的資料庫查詢或 CPU 密集運算
- 踩坑情境:
有些開發者為了貪快,直接在middleware.js中引入大型 ORM(如 Prisma)或直接連線 MySQL / MongoDB,想在路由攔截時順便撈取整筆使用者資料。 - 為什麼會出錯:
多數 Next.js 部署平台(如 Vercel)的 Middleware 是運行在 Edge Runtime 上的(基於 V8 Workers,而非完整 Node.js 環境)。Edge Runtime 不支援許多 Node.js 原生模組(例如fs、crypto的部分功能),且執行時間極短(通常有嚴格的逾時限制)。 - 排錯與解決方法:
症狀:部署到雲端平台時直接噴出編譯錯誤,或線上環境頻繁發生504 Gateway Timeout。
解法:Middleware 應只用來做輕量級的字串判斷(如檢查 JWT 的 Cookie 存在與否、格式是否合法、簡易重新導向)。若需複雜的資料庫驗證,應交由 API 路由或頁面本身的getServerSideProps處理。

3. 在 _app.js 內無條件執行會噴錯的瀏覽器 API
- 踩坑情境:
在_app.js的頂層直接讀取window.localStorage或document.cookie,用來初始化全域狀態。 - 為什麼會出錯:
_app.js在首次請求時會先在伺服器端(SSR)執行一次。此時伺服器環境根本沒有window物件。 - 排錯與解決方法:
症狀:ReferenceError: window is not defined(常見於本機測試正常,但一上線執行 SSR 就崩潰)。
解法:若要在_app.js中使用瀏覽器專屬 API,必須加上安全檢查,或包在useEffect內:
import { useEffect } from 'react';
export default function MyApp({ Component, pageProps }) {
useEffect(() => {
// 確保只在客戶端執行
const theme = window.localStorage.getItem('theme');
}, []);
return <Component {...pageProps} />;
}

4. 搞混 _app.js 與 _document.js 的樣式注入順序導致樣式閃爍 (FOUC)
- 踩坑情境:
使用 CSS-in-JS 框架(如 Styled Components 舊版)或 Material-UI (MUI) 時,沒有正確在_document.js收集伺服器端的樣式並注入<Head>。 - 排錯與解決方法:
症狀:畫面載入瞬間呈現無樣式的原始 HTML,0.5 秒後才突然變成有設計感的介面(Flash of Unstyled Content)。
解法:必須嚴格依照對應 UI 框架的官方文件,在_document.js的getInitialProps中攔截並收集樣式,確保 SSR 輸出的 HTML 已經帶有完整的 CSS。

💡 工程師避坑要點:何時該用哪個檔案?
middleware.js:管「能不能進來」(路由攔截、Edge 重新導向)。_document.js:管「外殼長怎樣」(全域 HTML 結構、字型、伺服器端靜態標籤)。_app.js:管「穿什麼衣服、帶什麼裝備」(全域狀態、Layout、Context Provider)。index.js:管「裡面賣什麼」(頁面專屬 UI 與資料)。

結論
掌握 Next.js 的載入順序與生命週期架構,能讓你在開發時清楚知道程式碼究竟是在伺服器端(如 _document.js 與 middleware.js)還是客戶端運行。妥善運用這四個核心檔案的職責劃分,可以有效避免權限漏洞、減少不必要的重複渲染,並打造出結構清晰且高效能的 React 應用程式。
發表迴響