
这一篇读的是 vercel/next.js。Next.js 很容易被写成框架百科:CLI、compiler、Turbopack、webpack loader、Pages Router、App Router、RSC、缓存、examples、测试全都扫一遍。这样读完通常只剩下目录名,真正遇到线上路由、RSC header、ISR 或 fetch cache 问题时,还是不知道该从哪里下手。
我更建议抓一条闭环主线:next build 先产出 manifests,生产 server 再读取这些 manifests 生成 matcher;请求进入 BaseServer 后先标准化 URL、识别 RSC/prefetch/header,并把 incremental cache 等上下文挂到 request meta;命中 app page 后进入 route module,再由 renderToHTMLOrFlight() 基于 loaderTree 生成 HTML 或 RSC Flight;最终 route response 和 render 内部的 fetch() 分别进入不同层次的缓存逻辑。
本文源码基线:本地目录 sources/next.js;分支 canary;提交 acb74eb551b72566217d39593e7f7fdb446d13e1;提交时间 2026-05-30 21:47:57 +0000;提交主题 [CLI] Exit non-zero with a clear message when next typegen fails (#94241)。根 package.json 声明 pnpm workspace,packages/next/package.json 声明 next = 16.3.0-canary.35。
本文由 AI 辅助整理和改写,可能存在遗漏、理解偏差或版本差异。关键实现请以本文固定的提交、对应源码文件和官方文档为准,自行复核后再用于工程判断。
先看 build,因为运行时靠 manifest 工作

App Router 的生产运行时不是每次请求都去扫描 app/ 目录。packages/next/src/cli/next-build.ts 的 nextBuild() 只是命令入口,真正的 build pipeline 在 packages/next/src/build/index.ts。
这条链路里值得盯住几个产物:routes-manifest.json、server/pages-manifest.json、server/app-paths-manifest.json、app-path-routes-manifest.json、prerender-manifest.json,以及 app page/app route 的 server bundle。handle-entrypoints.ts 也能看到 app page 和 app route entrypoint 会绑定 middleware manifest、build manifest、app paths manifest、action manifest、font manifest 等信息。
所以读 App Router,不能直接跳到 app-render.tsx。如果不知道 build 阶段写出了哪些 manifest,就解释不了生产环境里“某个 URL 为什么命中这个 route kind”“dynamic route 为什么按这个顺序匹配”“prerender/ISR 信息从哪里来”。
请求入口:BaseServer 先改造请求上下文

生产命令 next start 会进入 packages/next/src/cli/next-start.ts,再创建 server。custom server 的公开入口在 packages/next/src/server/next.ts,getRequestHandler() 会懒加载内部 request handler。真正的主线落在 packages/next/src/server/base-server.ts。
BaseServer 构造期会读取 pagesManifest、appPathsManifest、appPathRoutes,通过 getRouteMatchers() 注册 pages、pages API、app page、app route 的 matcher provider,并触发 this.matchers.reload()。请求真正进来时,handleRequestImpl() 先等待 matcher ready,再解析 URL、修正 repeated slash、补齐 x-forwarded-*,然后调用 attachRequestMeta() 保留原始请求信息。
RSC 也在这一层进入主线。handleRSCRequest() 会识别 RSC、prefetch、segment prefetch 等 header;后面还会解析 pathname info、locale、basePath、matched path。接近请求前半段末尾时,BaseServer 会创建或取得 incrementalCache,挂到 request meta,并保留一个 globalThis.__incrementalCache 兼容 pages router 的 unstable_cache。
这一步的意义很大:Next.js server request 不是简单的“URL 到 React 组件”。它先把 URL、RSC header、i18n、basePath、matched path、postponed state、incremental cache、server components HMR cache 等信息压进 request meta。后面的 route module 和 render pipeline 都在消费这些上下文。
Route Matcher:manifest 变成 static/dynamic 两组

App page matcher provider 在 route-matcher-providers/app-page-route-matcher-provider.ts,它从 APP_PATHS_MANIFEST 里筛出 app page route,并把 parallel routes 归并到同一个 pathname。App route matcher provider 在 app-route-route-matcher-provider.ts,对应 app/api/.../route.ts 这类 route handler。
合并器是 route-matcher-managers/default-route-matcher-manager.ts。reload() 会收集所有 provider 的 matcher,检测重复 pathname,然后拆成 static 和 dynamic 两组。dynamic route 还会通过 getSortedRoutes() 排序,保证 [id]、[...slug] 这类路径不会靠遍历顺序碰运气。
真正匹配时,matchAll() 先尝试 static matcher;如果没有要求跳过 dynamic,再按排序后的 dynamic matcher 逐个 validate()。底层 route-matcher.ts 反而很小:dynamic route 用 getRouteRegex() 和 getRouteMatcher() 提取 params,static route 做 pathname 相等判断。
这套结构解释了一个实用判断:如果你调试 Next.js 路由命中问题,不要先猜 React 组件加载错了。应该先看 manifest 里的 pathname、route kind、dynamic regex、params 提取,以及 static/dynamic 排序。
App Page:route module 把请求交给 RSC 渲染器

当 BaseServer.renderToResponseImpl() 进入渲染阶段,它会拿已有 match,或者调用 this.matchers.matchAll(pathname, options)。每个 match 会进入 renderPageComponent():这里会判断是否 app path,处理 parallel routes 的 root page,然后调用 findPageComponents() 加载 components 和 route module。
App page 的 route module 在 packages/next/src/server/route-modules/app-page/module.ts。它会处理 RSC URL normalization 和 request meta,核心 render() 再调用 renderToHTMLOrFlight(req, res, page, query, fallbackRouteParams, renderOpts, ...)。
packages/next/src/server/app-render/app-render.tsx 是 App Router 渲染核心,但不要把它当孤立文件。renderToHTMLOrFlight() 会解析 request URL,从 request headers 里读 RSC、prefetch、nonce、revalidated tags,处理 postponed state 和 interpolated params,再创建 WorkStore,用 workAsyncStorage.run() 进入 renderToHTMLOrFlightImpl()。
在 renderToHTMLOrFlightImpl() 里,几个动作串成了主干:安装 app bundle 的 module loading handlers;调用 ComponentMod.patchFetch() 把 render 内部 fetch 接到缓存体系;从 route module 的 userland 中取出 loaderTree;再根据 parsed request headers 判断 Flight router state、prefetch、RSC、HMR refresh、nonce。后面 generateDynamicRSCPayload()、walkTreeWithFlightRouterState()、createComponentTree() 才开始把 loaderTree 变成 Flight data、component tree 或 HTML stream。
换句话说,App Router 的页面渲染不是“找到 page.tsx 然后 render”。源码里真正的单位是 route module、loaderTree、request headers、WorkStore、AsyncLocalStorage 和 RSC Flight。
App Route:handler 执行前先建立动态性上下文
App Route 对应 app/api/.../route.ts,核心在 packages/next/src/server/route-modules/app-route/module.ts。这里绑定了 workUnitAsyncStorage、workAsyncStorage、actionAsyncStorage 和 server hooks。
handle() 会先确保 userland 加载完成,再按 HTTP method 找 handler。执行用户代码之前,它会创建 static generation context、把 userland 的 fetchCache 放进 render options、创建 ActionStore、implicit tags、request store 和 WorkStore。随后根据 dynamic 配置处理 force-dynamic、force-static、error、auto,必要时还会用 proxy request 跟踪 cookies、headers 等动态访问。
这段源码最适合拿来建立工程直觉:一个 route handler 是否能静态化,不只取决于文件位置。GET/POST、dynamic、fetchCache、revalidate、是否读取 cookies/headers、是否触发动态访问,都会在 WorkStore/RequestStore 里合流。
Response Cache 和 fetch cache 是两回事

缓存是 Next.js 源码里最容易讲混的部分。页面和 app route 的响应缓存入口在 packages/next/src/server/route-modules/route-module.ts,它会懒创建 ResponseCache,再通过 handleResponse() 调 responseCache.get(cacheKey, responseGenerator, context)。
ResponseCache.get() 的主线是:没有 cache key 直接生成;minimal mode 可先查内存 LRU;正常模式进入 batcher,避免同 key 重复生成;再从 incremental cache 取旧值。如果旧值存在且不是 expired,fresh 会直接返回,stale 会先把旧值返回给请求,同时继续 revalidate。revalidate() 重新生成后,会把 response cache entry 转成 incremental response cache entry,再写回缓存。
IncrementalCache 更底层,既处理 route response,也处理 FETCH kind。它会选择 cache handler,读取 prerender manifest,维护 revalidateAfter、tag、lock、resume data cache、SharedCacheControls。patch-fetch.ts 则把 globalThis.fetch 替换成 patched fetch:判断是否可缓存,生成 fetch cache key,按 IncrementalCacheKind.FETCH 查缓存;stale 命中时,非 static generation 可以先返回旧数据并安排后台刷新,static generation 则走前台 revalidate。
因此要把两个概念分开:route response cache 关心页面或 route handler 的响应,fetch cache 关心 render 期间的单个 fetch 请求。它们都接到 incremental cache,但 kind、key、生命周期和写入时机不同。
最后的判断:Next.js 的核心价值在运行时协议
读完这条链路,我对 Next.js 源码的判断是:它最值得学习的不是“React 框架如何组织目录”,而是一个现代全栈框架如何把 build artifact、路由匹配、RSC 协议、用户代码执行上下文和多层缓存连成一个运行时协议。
App Router 的主链路可以压缩成一句话:next build 写出 manifests;BaseServer 读取 manifests 创建 matcher,并在请求入口准备 request meta;Route Matcher 给出 route kind 和 params;App Page 通过 route module 进入 renderToHTMLOrFlight(),基于 loaderTree 输出 HTML 或 RSC Flight;App Route 在 WorkStore/RequestStore 中执行 handler;Response Cache 管 route 响应,patchFetch() 把 render 内部 fetch 接入 fetch cache、tags 和 revalidate。
如果继续读 Next.js,我会优先沿四个问题深入:build 产物如何决定运行时;URL 如何命中 route kind;RSC request headers 如何改变响应形态;route response cache 与 fetch cache 如何互相独立又共享 incremental cache。抓住这四个问题,Next.js 就不再是一堆庞大的目录,而是一条可追踪、可调试、可解释的运行时主链路。
夜雨聆风