Next.js 常被介紹成「帶有 file-system routing 與 SSR 的 React」。那個描述跳過了為什麼 'use client' 會改變 bundle、為什麼一個 request 回傳 HTML 而另一個回傳 text/x-component、為什麼 cookie 會改變工作何時發生,以及為什麼 invalidate server data 不一定會取代瀏覽器裡已有的內容。
有用的模型:Next.js 是圍繞 React 的 orchestrator。在 build time,它把一棵 source tree 轉成數個 module graphs。在 runtime,它選出一棵 route tree、協調 caches、請 React 產生 Server Component stream、可選擇把該 stream 轉成 HTML,並讓 client router 在不 reload document 的情況下 merge 稍後的 patches。
source modules
→ classify routes and server/client boundaries
→ emit server, client, and optional edge artifacts
→ match a request to a route tree
→ resolve cached and request-time work
→ produce a React Server Component payload
→ optionally produce and stream HTML
→ hydrate client boundaries
→ fetch and merge later route-tree patches這篇筆記針對 Next.js 16.2.11 App Router(這裡是 React 19.2.4)。三種陳述:
- 一條 React 規則,例如 render purity,或 client-bound values 必須 serializable。
- 一份 Next.js 契約,例如
page.tsx或cacheTag。 - 一項 實作觀察,例如 manifest field 或內部 header。有助於除錯。應用程式碼不得依賴未文件化的格式。
App Router 不會執行 getStaticProps 或 getServerSideProps。Rendering 與 caching 在 route、component 與 function boundaries 組合。
1. Next.js 在 React 周圍加上 Policy 與基礎設施
React 定義 components、reconciliation、Suspense、Server Components 與 hydration。它不決定 URLs 如何對應到 trees、component 在哪裡執行、route 如何被 cache,或 server function 如何透過 HTTP 定址。
Application source
→ Compiler and bundler
→ Route manifests and runtime bundles ─┐
HTTP request or navigation ──────────────┼→ Next.js runtime
Deployment adapter and infrastructure ───┘
├→ React server renderer
│ → HTML, RSC payload, and client chunks
│ → Browser and client router
└→ Shared cache, assets, and functions
Deployment adapter also → Shared cache, assets, and functions- Compilation 決定哪些 modules 可以出現在 browser chunk,並在 server 與 client graphs 之間建立 references。
- Routing 把 pathname 轉成 layouts、pages、slots 與 boundaries。
- Rendering policy 決定哪些工作可以在 request 之前發生、哪些必須在 request 期間發生,以及 Suspense 在哪裡切開兩者。
- Caching policy 決定哪些 results 可以重用、給誰、多重久。
- Deployment adapter 把 output 對應到 processes、functions、CDN objects 與 shared caches。Vercel、長時間運行的 Node server,以及 AWS 上的 OpenNext,並不相同。
這個 repository 的 next.config.ts 啟用 React Compiler、組合 Fumadocs 與 next-intl、定義 PostHog rewrites,並設定 image policy。packages/infra/nextjs.ts 再把 build 交給 SST 與 OpenNext。
耐用的模型是 compiler 加上 request-time coordinator 加上 client router,再透過 host-specific adapter 部署。
2. Route Tree 不只是 URL Matcher
app 底下的 folders 定義 segments。Special files 把行為掛到那些 segments。
app/
layout.tsx root layout
[locale]/
layout.tsx locale layout
loading.tsx segment loading boundary
error.tsx segment error boundary
notes/
page.tsx /:locale/notes
[slug]/
page.tsx /:locale/notes/:slugpage.tsx讓一個 segment 可被定址。layout.tsx包住 descendants,並在該 segment 仍匹配時跨 navigations 保持 identity。template.tsx佔類似位置,但在 navigation 時拿到 新的 identity,所以 client state 與 Effects 會重啟。loading.tsx、error.tsx與not-found.tsx在定義好的位置成為 boundaries。它們不是全域 event listeners。- Route groups 如
(marketing)只做組織,不增加 URL segment。[id]、[...rest]與[[...rest]]貢獻 parameters。@modal是 parallel slot。(.)photo攔截 client navigation,同時保留一條 canonical 的 direct-load route。 - Build manifests(
app-paths-manifest.json、route regexes)解釋 runtime 行為。它們的 schema 是 internal。受支援的介面仍是 file conventions。 - 在
/en/notes/a→/en/notes/b期間,共享 layouts 可以保持 mounted。Client router 套用的是一份 server tree patch,不是一個新的 React root。
Root layout → Locale layout → Notes segment → slug: a
Notes segment ─(next navigation patch)→ slug: bFailure: 把 URL observation 放進 server layout,並指望它看見每一次 navigation。Pathname 與 search params 屬於帶有 usePathname 或 useSearchParams 的 Client Component。Layout persistence 是功能,不是 bug。
3. Document Request 與 Navigation 是不同的 Pipelines
一次 document request 匹配一條 route、解析 caches、render 一份 RSC payload、把它轉成 HTML、在 Suspense boundaries 就緒時 stream bytes,然後 hydrate Client Components。之後的 client navigation 通常 不會 再請求另一份 HTML document。
Document request:
Browser → Next server: GET document URL
Next server → React: Render matched route tree
React → Next server: RSC stream
Next server → Browser: HTML + embedded RSC data + chunk references
Browser: Paint HTML and hydrate client boundaries
Client navigation:
Next client router → Next server: Request destination RSC payload
Next server → React: Render destination tree
React → Next server: RSC tree patch
Next server → Next client router: text/x-component response
Next client router: Merge patch and reconcile- Client router 送出目前的 tree state,請求到達 destination 所需的 RSC data,然後 merge 這份 patch。
- 本地
routes-manifest.json記錄rsc作為 request discriminator、text/x-component作為 RSC content type,以及Vary於rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch。 - 那些名字是操作證據,不是 API。Proxy 或 CDN 若剝掉它們,可能 cache 錯誤的 variant。
proxy.ts在 route render 之前、response cache 能回答 之前 跑。便宜的 redirects、rewrites、粗粒度 gates。不是讀取或突變 data 的那段程式碼的 authorization。- Next.js 16 把
middleware.ts改名為proxy.ts。這個 repository 的src/proxy.ts只跑next-intllocale routing,並設localeDetection: false,讓/redirects 對 CDN caching 保持可確定。
4. 'use client' 切開 Module Graph
Server Components 是預設。這個短語不代表「每次 HTTP request 跑一次」。它代表實作留在 server graph,而 rendered result 穿過 RSC boundary。
'use client' 是 module-graph entry directive。
'use client'
import { useState } from 'react'
export function Quantity() {
const [value, setValue] = useState(1)
return <button onClick={() => setValue(value + 1)}>{value}</button>
}page.tsx server module → ProductDetails server module
page.tsx server module → Quantity client reference ─(manifest mapping)→ Quantity browser module → react-dom client runtime
page.tsx server module → Database-only module- 這個 directive 在從 server graph import 時,把這個 module 的 exports 標成 client references。除非另一條 boundary 切開 graph,transitive imports 必須對 client 可達。
- 它 不 代表:沒有 server HTML、每個 descendant 都是 client module、component 可以 import server secrets,或瀏覽器可以呼叫任意 server functions。
- Server 可以 render
<Quantity />,因為 RSC stream 編碼了一個 reference 加上 serializable props。Database module 留在 server graph。 - Client Component 可以透過
children接收 Server Component result。它並不是在瀏覽器裡 import 並執行那個 server module。 - 穿過 boundary 的 values 必須 serializable。Server Functions 是刻意的例外:一個 Next.js 知道如何 invoke 的 reference。
- 用
server-only與client-only,讓洩漏在 compile time 失敗。
Failure: 把 'use client' 放在高層 shell。Providers、utilities 與 descendants 會被拉進 browser-reachable chunks。把 directive 放在 窄的 interactive leaves。
5. Flight 攜帶一棵 Tree,HTML 攜帶初始畫面
RSC payload——常稱為 Flight payload——不是 HTML,也不是 DOM 的 JSON 畫面。它是一條 stream:已 render 的 Server Component output、pending Suspense 工作、Client Component references,以及 serializable values。
RSC render
├─ RSC payload: authoritative server-rendered React tree and references
├─ HTML: initial visual representation produced from that result
└─ client chunks: executable code for Client Component references- HTML 讓瀏覽器在 application JavaScript 執行之前就能 paint。RSC 重建同一棵邏輯 tree。Client chunks 為 interactive boundaries 提供行為。
- Hydration 作用在 Client Components,不是 Server Component 的實作。瀏覽器收到的是 Server Component 的 rendered result,不是它的 function body。
- 「Zero JavaScript」是 per-subtree 的可能。靠近 root 的一個 client provider 仍可能加寬 interactive 表面。
- 之後的 navigations 跳過 HTML,因為瀏覽器已經擁有 document。把 RSC endpoint 當公開 JSON API 是錯的:它耦合到該 build 的 module references 與 router state。
Failure: 在產品程式碼裡 parse Flight bytes 或產生出來的 client-reference maps。檢查它們來診斷 proxy、cache 或 bundle 問題。它們不是穩定的應用介面。
6. Rendering 是光譜,不是三種 Page Types
Pages Router 詞彙——CSR、SSR、SSG、ISR——仍描述結果。App Router 的工作組合得更細,落在三個時間:build 或 revalidation、request,以及 client。
import { Suspense } from 'react'
export default function ProductPage() {
return (
<>
<ProductCatalog />
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</>
)
}- 沒有 Cache Components 時,Next.js 仍可以分類並 prerender routes。自 Next.js 15 起,
fetch預設 不會 放進 persistent cache。 - 打開
cacheComponents: true後,一條 route 可以混合 prerendered 工作、'use cache',以及讀取cookies()、headers()或searchParams的 request-time 工作。 - Request-time 工作必須坐在 Suspense 下面,build 才能發出 static shell——Partial Prerendering。
- Suspense 是 streaming boundary。
'use cache'是在某個 key 與 lifetime 下的 reuse。 - 這個 repository 使用
dynamic = 'force-static'、dynamicParams = false與generateStaticParams。它 沒有 啟用cacheComponents。這裡的 caching 是 build-time SSG 加上 CDN。
Failure: 指望 Suspense 去 cache 一次 query,或指望 cached data 自動 stream。
7. Data Dependencies 決定 Streaming 品質
一個 async Server Component 可以直接 query database。把這次呼叫繞過這個應用的 Route Handler,會加上 HTTP serialization、另一次 router pass,以及一次取決於部署的 hop。改成共享一個 server-only function。
const productPromise = getProduct(id)
const inventoryPromise = getInventory(id)
const [product, inventory] = await Promise.all([
productPromise,
inventoryPromise,
])- 獨立的
await getProduct再await getInventory會把本可重疊的工作 序列化。先啟動兩者,再Promise.all,或把它們放在 sibling Suspense boundaries 下揭示。 - Streaming 修不好真正的 data dependency。它可以修好一個 parent 在構造下一個 child 之前就 await 了前一個。
- Request memoization 在一次 render 裡去重等價工作。Persistent cache 跨 requests 重用 result。一次 memoized 但未 cache 的 query 下次仍會再跑;persistent cache 仍可能回傳 stale data。
- 瀏覽器擁有的 data——live collaboration、高頻 polling、offline state、需要 browser credentials 的第三方 APIs——仍屬於 client。
Failure: 對獨立 reads 做 sequential await。那條 waterfall 是架構引入的,不是固有的。
8. Caching 是一組 Lifetimes 與 Identities
「這個 page 被 cache 了嗎?」是錯誤的問題。一個 Next.js 應用可以在多個 scopes 重用工作。
one render/request
request memoization deduplicates equivalent reads
many requests
a data or function cache reuses a result by key
route output
prerendered HTML and RSC output can be reused
one browser session
the client router reuses prefetched and visited route segments
network edge or host
a CDN may reuse HTTP responses according to host policy- 每一層有自己的 identity 與 invalidation。一行新鮮的 database row 不保證已經坐在 Router Cache 裡的 RSC response 也是新鮮的。
router.refresh()可以請求新的 server result,而不刪除 persistent data-cache entry。 - 用
cacheComponents: true啟用 Cache Components。然後'use cache'加上cacheLife與cacheTag,讓一個 function 選擇進入 persistent reuse。
import { cacheLife, cacheTag } from 'next/cache'
async function getProduct(id: string) {
'use cache'
cacheLife('hours')
cacheTag(`product:${id}`)
return db.product.findUniqueOrThrow({ where: { id } })
}- Cache key 包含 build identifier、secure function identifier、serialized arguments,以及從 outer scope 捕捉到的 serialized values。Tags 是 invalidation index,不是主要 identity。
- Runtime request APIs 不能在
'use cache'裡面呼叫。在外面讀cookies(),只傳入應參與 identity 的那個值。按 session ID cache 在機制上有效,但常常是壞政策。 cacheLife視窗:stale(重用不檢查)、revalidate(先 serve stale 同時刷新)、expire(必須等新鮮工作)。revalidateTag(tag, 'max')標成 stale。updateTag立刻 expire,用於 read-your-own-writes,並限制在 Server Actions。revalidatePath針對 route output;它不取代 scoped data tags。
Read getProduct(id)
→ Request memoization
→ Function cache key
→ Rendered RSC result
→ Browser Router Cache
Server Action mutation
├→ updateTag or revalidateTag → Function cache key
└→ Refresh affected route tree → Rendered RSC resultSelf-hosting 會把它變成分散式系統問題。Process-local cache 在 replicas 之間並不 coherent。Next.js 支援 custom cache handlers;host 仍必須協調 invalidation 與 build IDs。
Failure: tag 寫成 product:${productId},而 function 還吃 tenantId。Reads 仍按 arguments 隔離;invalidation 寬度錯了。在共享 cached 工作裡從 ambient mutable state 讀 tenant,會讓 isolation 無法審計。
9. Navigation 套用 Server 產生的 Tree Patches
<Link> 不只是帶 preventDefault() 的 anchor。它給 router 一個可以 prefetch、cache,並在保留共享 segments 的同時 transition 過去的 destination。
Viewport → Router: Link becomes eligible for prefetch
Router → Server: Prefetch route or segment RSC data
Server → Router: Cacheable RSC patch
Router: Store prefetch entry
Viewport → Router: User activates Link
Router: Reuse entry or request missing data
Router → React: Apply server tree patch in a transition
React: Preserve matching layouts and client state- Prefetching 是 speculation。Render 必須能安全地開始、重複、cache 與丟棄。不要把 mutate 當 render 的 side effect。
router.push與router.replace請求另一種 route-tree state。back與forward接入 browser history。refresh重新請求當前 route 並 merge,保留相容的 client state。usePathname與useSearchParams觀察 client router state。router.refresh不是通用 invalidation API。若 server 工作被 persistent cache,refresh 可以忠實地回傳同一個值。在 write boundary invalidate,然後再 refresh。
Failure: 把不受信任的 strings 拼進 router.push 或 router.replace。把 destinations 當 injection boundary;javascript: URL 可以在 page context 執行。
10. Server Actions 是有位址的 Mutations,不是受信任的 Functions
Server Action 是作為 mutation entry point 使用的 Server Function。'use server' 建立一個可以穿過 RSC boundary 的 server reference。
'use server'
import { updateTag } from 'next/cache'
import { z } from 'zod'
import { requireEditor } from '@/lib/auth'
import { db } from '@/lib/db'
const Input = z.object({
id: z.string().uuid(),
name: z.string().trim().min(1).max(120),
})
export async function renameProduct(formData: FormData) {
const actor = await requireEditor()
const input = Input.parse(Object.fromEntries(formData))
await db.product.update({
where: { id: input.id, tenantId: actor.tenantId },
data: { name: input.name },
})
updateTag(`product:${actor.tenantId}:${input.id}`)
}- Client 發送一次 HTTP POST,攜帶 action reference 與 serialized arguments。未使用的 actions 可以在 build time 被消除。
- 從 client 可達的 action 是 可被外部呼叫的 entry point。即使 TypeScript 另有說法,arguments 仍是 hostile input。
- Authentication 證明 identity。Authorization 證明這個 identity 可以對這個 resource 執行這個操作。Captured variables 不是 authorization 系統。IDs 是 opaque,不是 secret capabilities。
- Same-origin checks、encrypted closures、origin allowlists 與 body limits 縮小攻擊面。它們不決定 tenant A 是否可以編輯 tenant B 的 row。
- Mutations 仍需要 idempotency keys、optimistic concurrency、transactions、commit 之後 的 invalidation、不洩漏 secrets 的 error mapping,以及 observability。
- HTML forms 可以在 hydration 之前 invoke actions。Server 仍是權威。
11. 選最窄的 Server Entry Point
Server Components、Server Actions、Route Handlers 與 Proxy 都在 server 上執行。它們解決不同的問題。
- Server Component: 為了 render 一棵 React tree 而存在的 reads。可直接存取 server-only modules。不是公開 protocol boundary。
- Server Action: 由這個 React 應用發起的 mutations。把它當私有 HTTP endpoint。
- Route Handler: 明確的 HTTP resource——webhooks、公開 APIs、files、feeds、非 React clients。Web
Request/Response。一個route.ts不能與page.tsx佔同一 segment。 - Proxy: route handling 之前的攔截。保持便宜。它在每個匹配的 request 上跑。它不取代受保護 data 旁邊的 authorization。
Server Component ─┐
Server Action ────┼─→ shared server-only domain function → database/service
Route Handler ────┘Failure: Server Component → HTTP fetch 到這個應用的 Route Handler → domain function,拿來當通用 reuse。HTTP boundary 只有在那個 boundary 本身就是被測試或被消費的能力時才成立。
12. Dynamic APIs 把工作綁到 Request
在 Next.js 16 裡,params、searchParams、cookies() 與 headers() 是 asynchronous。
import { cookies, headers } from 'next/headers'
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ preview?: string }>
}) {
const [{ id }, query, cookieStore, headerStore] = await Promise.all([
params,
searchParams,
cookies(),
headers(),
])
// ...
}- 這個 async 形狀讓 Next.js 可以推遲 request-bound 工作。讀一個 cookie 無法為每個使用者產出一份共享的 prerendered 值。
- 有 Cache Components 時,request-time APIs 屬於
'use cache'之外,通常在 Suspense 下面。沒有 Cache Components 時,它們會讓相關 route 變成 dynamic,除非 configuration 另有說明。 - Page 上的
searchParams是 request data。useSearchParams是 client router hook。相關的值,不同的執行與 rendering 後果。 - 預設 runtime 是 Node.js。Edge 提供較小環境的 Web APIs。Cache Components 需要 Node,不支援
runtime = 'edge'。「更靠近使用者」不會自動勝過 database 距離、cold starts,或不被支援的 dependencies。 NEXT_PUBLIC_*變數可能在 build time 被替換進 client bundles。它們是公開的。Client import 無法保住 secret。
13. Loading 與 Failure 跟隨 Segment Boundaries
App Router 把 control-flow 結果變成 route-tree 行為。
loading.tsx為一個 segment 提供 Suspense fallback。error.tsx為它下面未捕捉的 failures 提供 Client Component error boundary。global-error.tsx在 root layout 失敗時替換它,必須自己 render<html>與<body>。not-found.tsx在其 boundary 處理notFound()與未匹配的 resources。redirect()與permanentRedirect()終止當前 rendering 路徑。
try {
const product = await getProduct(id)
if (!product) notFound()
} catch (error) {
unstable_rethrow(error)
}- 不要吞掉 framework control-flow errors。把
try收窄到可能失敗的那次操作,或用unstable_rethrow再拋出。 - Error boundary 不會讓失敗的 mutation 變成 transactional。Retry UI 不會讓 action 變成 idempotent。已經 commit 到外部系統的 side effects 仍然 committed。
Failure: 一個過大的 Suspense boundary 拖住獨立內容,或幾十個過小的 boundaries 造成視覺 churn。按 reveal 順序與 latency domain 放置 boundaries,而不是圍住每個 async function。
14. Advanced Routing 用簡單換 State Preservation
Parallel 與 intercepting routes 解決的是 URL、可見 layers 與 navigation history 不該對應到一棵簡單 page tree 的情況。
app/
@modal/
default.tsx
(.)photo/[id]/page.tsx
photo/[id]/page.tsx
layout.tsx- Soft navigation 可以把
(.)photo/[id]render 進當前 gallery 上的@modalslot。對/photo/123的 direct request 渲染 canonical page。router.back()恢復 history。default.tsx在 full reload 之後提供 fallback。 - 若 modal 不需要可分享 URL、reload 語意或 history 整合,本地 state 更簡單。
- 這個站點用
[locale]作為第一個 dynamic segment,並用next-intl做 validation、messages 與 navigation。Locale layout awaitparams,對不支援的 locales 以notFound()拒絕,並呼叫setRequestLocale,讓本來 static 的 routes 可以按 locale 生成。 - Rewrites 保持 browser URL。Redirects 改變它。Route groups 兩者都不改。這個 repository 把
/ingest/*proxy 到 PostHog,同時讓瀏覽器留在 first-party origin。
15. Metadata 與 Asset Pipelines 是 Render Contract 的一部分
Metadata 從 route tree 解析:static metadata exports、generateMetadata,以及 opengraph-image、icon、robots 這類 file conventions。Parent metadata 與 child metadata merge;一份不完整的 leaf export 不會抹掉 parent object。
generateMetadata可以 await 與 page 相同的 server data。共享底層 promise 或 cached function,這樣 metadata 就不會把 body 序列化,除非那份工作已經被 memoized。next/image產生定尺寸 variants、強制設定的 remote hosts,並注入srcset/sizes。不受限的 remote host list 會變成 open image proxy。next/font自託管 fonts、生成 subset CSS,並去掉常見的第三方 render-blocking request。Font metrics 與display仍決定 layout shift。next/script控制第三方 JavaScript 何時爭奪 main thread。Strategy 是效能決策,不是語法決策。- Image optimization 可能在 function、edge 或 CDN 後面跑,取決於 host。公開 API 仍是
next/image;營運成本不是。
16. Compilation 把一棵 Source Tree 變成多份 Artifacts
一次 Next.js build 不是「bundle 這個 app」。它分類 modules、發出多份 graphs,並寫出 runtime 稍後用來回答 requests 的 manifests。
app and shared modules → Classify routes and directives
├→ Server module graph → Server route and RSC chunks ─┐
├→ Client module graph → Browser chunks and CSS ──────┼→ Deployment adapter
├→ Optional edge/proxy graph → Proxy/edge handlers ───┤
└→ Route, client-reference, and build manifests ──────┘- SWC 轉換 TypeScript/JSX。這個 repository 啟用的 React Compiler 可以自動 memoize 符合條件的 components。它不取代 Server Components、Suspense 或 cache policy。
- Turbopack 是 Next.js 16.2 的預設 bundler。這個 repository 的
next dev與next buildscripts 不傳--webpack。 - 診斷 production 行為時,檢查 route manifests、client-reference metadata、build identifiers 與 chunk lists。
NEXT_PUBLIC_*值可以被 inline 進某次 build 的 client assets。輪換一個公開 analytics key 需要重建那些 assets。執行時讀取的 server-only secret 則不必。- OpenNext 把 artifacts 對應到 host resources。這個 repository 的
open-next.config.tsoverride tag cache、incremental cache 與 queue backends(DynamoDB/S3/SQS lite),並使用 Lambda streaming wrapper。以目前完全 static 的表面,那些 adapters 大多閒置。用 SST 管理 AWS 基礎設施與 DevOps 說明 SST 如何接上 adapter。 - Rolling deploys 必須讓 build IDs 與 client references 對齊,這樣舊的 browser session 才不會去呼叫一個已經不理解其 action IDs 的 server。CDN layers 必須尊重 content-type 與
Vary。這個站點把 Orama search indexes 當 static JSON 提供,因為 OpenNext on Lambda 繼承了同步 response size 上限,讓/api/search變得脆弱。
17. Security 與 Observability 落在每個 Boundary
Next.js 把 code 與 data 移過數個 trust boundaries。當那些 boundaries 被當成 framework 魔法時,安全工作就會失敗。
| Boundary | What crosses it | Required controls |
|---|---|---|
| Browser ↔ Server Action / Route Handler | Hostile input and cookies | Authn, authz, validation, rate limits, CSRF/origin policy |
| Server Component → Client Component | Serialized props and references | No secrets, no privileged objects, intentional public data |
| Proxy → Route | Request headers and redirects | Cheap checks only; no sole authorization |
| App → External service | Credentials and PII | Scoped secrets, least privilege, audited egress |
| CDN ↔ Origin | Cached responses | Correct Vary, no private HTML in shared caches |
- Authentication 回答「這是誰?」Authorization 回答「這個 actor 可以對這個 resource 執行這個操作嗎?」用 schema parse input 之後,在 mutation 或 Route Handler 裡面 validate 並 authorize。
- Secrets 屬於 server-only modules 與 runtime configuration。Server Actions 裡 closed-over 的值有傳輸保護,但一旦 action 可達,它們仍是可被外部呼叫介面的一部分。
- Defense in depth 仍然適用:CSP、origin allowlists、body-size limits、記錄 actor/resource/outcome 而不 dump secrets 的 logs、給 error tracking 用但不成為公開 dump 的 source maps。
instrumentation.ts是註冊 OpenTelemetry 的受支援位置。用同一套 trace model 關聯 document requests、Flight navigations、Server Actions 與 Route Handlers。
18. Performance 是 Boundaries 的架構
多數 Next.js 效能失敗都是 boundary 錯誤。
- 樹裡過高的
'use client'加寬 hydration 與 JavaScript。 - Sequential awaits 把獨立 latency domains 序列化。
- 缺少 Suspense 迫使整個 response 等最慢的區域。
- 過度 cache 個人化 data 會造成巨大的 key cardinality 或隱私事故。
- 共享 data 快取不足,會把每次 navigation 變成 origin 工作。
- 只量本地
next dev會藏起 production chunking、compression 與 cache 行為。
- LCP 關心關鍵 HTML、fonts 與 images 是否能盡早被發現,以及 server 工作是否擋住最大的內容。
- INP 關心 hydration、client JS 與 handlers 對 main thread 的爭奪。
- CLS 關心預留的 image/font 空間與晚注入的 UI。
- 一個 subtree 的 time-to-interactive 關心那個 subtree 拉進了多少 Client Component JavaScript。
Browser critical path 仍統治最後一公里。見 Browser 裡的 Critical Rendering Path。至於 React 的 speculative render、commit 與 hydration,見 深入理解 React。
量 production builds:document 對比 text/x-component、client chunk size、一張 image 是否走了 /_next/image,以及一次 mutation 是否造成預期的 cache miss。
19. 從頭到尾跟一頁 Product Page
一個 product page 展示給所有使用者 cache 的共享 catalog fields、基於 session cookie 的個人化 recommendations,以及一份需認證的 rename form。
Browser → CDN: GET /products/123
CDN → Server: Document request on miss
Server → Cache: Read cached ProductDetails
Cache → Server: Shared product fields
Server → Browser: Static shell HTML + RSC + fallbacks
Browser: Paint title and hydrate client boundaries
Server → DB: Fetch recommendations with session
Server → Browser: Stream personalized region
Browser → Server: Prefetch sibling product via Link
Server → Browser: RSC patch stored in Router Cache
Browser → Server: POST rename Server Action
Server → DB: Authorized update
Server → Cache: updateTag product 123
Server → Browser: Action result and refreshed route data- 共享的
ProductDetails使用'use cache'+cacheTag。Recommendations 在 cache 外面、Suspense 下面讀cookies()。 - Document 可以在 recommendations 完成之前 paint product title。
- 授權去 render form,不等於授權去 跑 action。Action 會再檢查、validate、寫入,然後
updateTag。 - 到另一個 product 的 client navigation 請求 RSC data,而不是完整 HTML document,除非 adapter 或 link configuration 另有強制。
- 兩位 editors 並發 rename 需要應用層 concurrency 策略。Next.js 不會發明一個。
- 部署之後,舊 tab 可能無法 invoke action,直到它 reload 到新 build 的 references。
Failure: 把 cookies() 放進 'use cache'——build 應該拒絕這種組合。按 session ID cache 卻沒有隱私審查:機制有效,政策仍可能是錯的。
20. 除錯問題與常見誤解
當 Next.js 行為讓你意外時,按這個順序問:
- 這是哪條 pipeline? Document request、Flight navigation、prefetch、Server Action、Route Handler,還是 proxy?
- 哪個 graph 擁有這個 module? Server、client,還是 edge?
- 這個值何時被計算? Build/revalidation time、request time,還是 client time?
- Cache identity 是什麼? Function arguments、captured values、tags、route path、build ID,以及 CDN variant。
- Network 實際回傳了什麼? HTML、
text/x-component、JSON、redirect,還是帶錯誤Vary的 cached CDN object? - React 是 commit 了,還是只嘗試了一次 render? Streaming 與 transitions 可以放棄工作。
- Authorization 是否在 write boundary 強制執行? UI 隱藏不是 enforcement。
- Deployment adapter 是否保留 headers、content types 與 shared cache 語意?
常見誤解:
- Next.js 不只是 SSR。 App Router routes 組合 prerendered、cached、streamed 與 request-time 工作。
'use client'不代表只在 client render。 它標記一條 module-graph boundary。Component 仍可以 SSR 成 HTML,然後再 hydrate。- 自 Next.js 15 起,
fetch預設不被 cache。明確的 Cache Components 與 cache APIs 定義 reuse。 - Server Actions 是可達的 mutation endpoints。 每次呼叫都要 validate input 並 authorize。
- Dynamic 不代表沒有 caching。 Request-time 區域可以坐在 cached 區域旁邊。
- Edge 不總是更快。 當 workload 適合 runtime、瓶頸是靠近使用者時,它才有幫助。
- RSC 不取代 APIs。 公開 clients、webhooks 與非 React consumers 仍需要 Route Handlers。
- Fiber/Flight 細節不是應用 APIs。 有助於診斷;作為產品契約則不穩定。
21. 最終心智模型
最短且準確的模型:
Folders define a route tree.
Directives partition module graphs.
Server Components assemble data into a Flight tree.
Caches reuse work by explicit identity and lifetime.
Suspense divides what can stream independently.
HTML is an initial picture.
Flight is the authoritative server tree and references.
Client Components hydrate and own interaction.
Actions mutate through addressed server entry points.
Invalidation reconnects writes to later reads.
Adapters map artifacts onto infrastructure.Next.js 不取代 React 的 rendering model。它決定哪些 React trees 對應哪些 URLs、它們的 modules 在哪裡執行、哪些 results 可以重用、那些 results 如何穿過 network,以及瀏覽器如何把稍後的 server output merge 進已經 mounted 的 document。
Compile-time classification、request-time coordination 與 client navigation 是三個不同的階段。多數 production incidents 來自把它們壓成一個模糊的「這個 page rendered」。
至於 React 自身的 render/commit/hydration 機制,繼續看 深入理解 React。Browser pixels 與 main-thread 限制見 Browser 裡的 Critical Rendering Path。這個 repository 的 AWS/OpenNext 交付路徑見 用 SST 管理 AWS 基礎設施與 DevOps。Server Components 所依賴的 React 19 APIs 見 React 19 新功能。Origin threat model(cookies、Server Actions、cache vs privacy)見 Next.js 裡的 Security。
這篇文章裡的實作觀察對應這個 repository 安裝的 Next.js 16.2.11 App Router 行為。公開慣例——file names、文件化的 directives、cache APIs,以及受支援的 runtime functions——才是耐用契約。Manifest layouts、內部 headers 與確切的 Flight bytes 是今天 toolchain 的除錯證據,不是該寫死的 API。