Nextjs 是如何实现 production ready 的 react

production ready 不是一个功能,是一批被预先做掉的决定。拆开 next@14 看三个能对上号的:预渲染方式在构建期固化成 __N_SSG / __N_SSP,framework 和 lib 是两组判据不同的 chunk,以及 App Router 之后这些还剩多少。

预计
6 分钟

「production ready 的 React」不是某一个功能。React 自己只管把组件渲染成 DOM,剩下的每一件事 —— 这个页面该在构建时生成还是每次请求现渲、哪些依赖该单独打成一个长缓存的包、页面组件怎么从磁盘上找出来 —— 都得有人替你决定。Next.js 卖的就是这批决定。

下面挑三个能自己对上号的。本文所有源码和常量都取自 next@14.2.35,装完之后在 node_modules/next/dist/ 下面直接搜得到,文中给的路径都是编译后的产物路径。讲的是 Pages Router;App Router 是另一套,差别放在最后一节。

预渲染方式在构建期就固化成两个标记

一个页面是静态生成还是每次请求渲染,不是运行时判断的,构建时就定死了。定死的形式是页面模块上的两个属性:

next/dist/shared/lib/constants.js
export const STATIC_PROPS_ID = '__N_SSG'
export const SERVER_PROPS_ID = '__N_SSP'

构建时 Next 检查页面导出了 getStaticProps 还是 getServerSideProps,把对应的标记打在页面上。之后服务端拿到请求,看的是这个标记,而不是再去分析一遍你的代码。

自己验一眼,不用建项目:

这两个常量的真实取值
node -p "require('next/dist/shared/lib/constants.js').STATIC_PROPS_ID" # __N_SSG
node -p "require('next/dist/shared/lib/constants.js').SERVER_PROPS_ID" # __N_SSP

这个设计的代价,就是「构建期决定」这五个字本身:同一个页面没法今天走静态、明天走动态。要改只能重新构建。ISR 的 revalidate 是在这个前提下开的一道口子 —— 它没有把决定挪到运行时,只是给静态产物加了个过期时间。

framework 和 lib 是两组,只有 lib 看体积

Next 预先写好的 splitChunks 分组,是「production ready」里最能直接抄的一份答案。这里有个流传很广的说法要纠正:framework 组不是按体积挑的。

packages/next/src/build/webpack-config.ts · Next 14 的两个 cacheGroup
const frameworkCacheGroup = {
chunks: 'all',
name: 'framework',
// Ensures the framework chunk is not created for App Router.
layer: isWebpackDefaultLayer,
test(module) {
const resource = module.nameForCondition?.()
return resource
? topLevelFrameworkPaths.some((pkgPath) => resource.startsWith(pkgPath))
: false
},
priority: 40,
// Don't let webpack eliminate this chunk
enforce: true,
}
const libCacheGroup = {
test(module) {
return (
!module.type?.startsWith('css') &&
module.size() > 160000 &&
/node_modules[/\\]/.test(module.nameForCondition() || '')
)
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true,
}

两组的判据完全不同。framework包的路径匹配:topLevelFrameworkPaths 是 Next 从 reactreact-dompackage.json 位置递归解析出来的一组目录前缀,模块的路径以其中之一开头就算数。源码里的注释特意说明了只收顶层的包,node_modules/xxx/node_modules/react 这种嵌套副本不算 —— 嵌套副本跟着引用它的人走,否则会被拆进一个谁都用不上的 chunk。

lib 才是看体积的那一组:不是 CSS、大于 160000 字节、路径在 node_modules 里,三条同时成立。它的 name 是对模块标识算 SHA-1 取前八位,所以产物里那些看着像乱码的 chunk 文件名就是这么来的。

最直接的反证来自一次真实构建:拿两个页面跑 next build,产物里的 framework-*.js 是 139,978 字节 —— 不到 160,000。要是 framework 真按体积挑,React 压根进不了这个 chunk。

priority 40 比 30 高,意味着 React 即使涨过 160KB 也不会掉进 lib,它归 framework。分开的理由是缓存周期不一样:React 的版本几个月才动一次,业务依赖的大包换得勤,混在一个 chunk 里,后者一变前者的缓存跟着失效。

framework 组上那行 layer: isWebpackDefaultLayer 和它上面的注释值得留意 —— 这个 chunk 在 App Router 下根本不建。 下一节说为什么。

手动切分的出口只有一个,跟这套自动分组是两件事:

按需加载一个组件
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('../components/Chart'))

页面在磁盘上,请求进来才 require

文件系统路由的服务端一侧,做的事情朴素得超出多数人预期:把 URL 算成一个磁盘路径,然后 require 它。干这活的函数叫 requirePage,在 next/dist/server/require.js

顺着这条链能看到几个实现细节。next/dist/server/utils.js 里有个 isBlockedPagebase-server.js 在算路径之前先用它拦一道,挡掉 /_document 这类不该被直接访问的内部页面。API Routes 走的是另一条分支,最后落到 next/dist/server/api-utils/node/api-resolver.jsapiResolver —— 它拿到你在路由文件里 export default 的那个处理函数,把 req / res 递进去,并且负责把抛出来的异常兜住转成响应,而不是让整个 Node 进程崩掉。

「兜住异常」这件事本身就是 production ready 的一部分:裸写 Node HTTP 服务时,handler 里一个未捕获的 rejection 能把进程带走。

App Router 之后这些还剩多少

上面三节全是 Pages Router 的结论。App Router 从 Next 13.4 起标记为稳定,它换掉的东西比多数人以为的多:

  • getServerSidePropsgetStaticPropsgetStaticPathsgetInitialProps 在 App Router 里都不存在。 不是不推荐,是不支持 —— 官方迁移文档里写的是这几个函数「已被替换」。对应关系里只有 getStaticPaths 有个直接的继任者:generateStaticParams
  • __N_SSG / __N_SSP 那套标记不适用。 App Router 里静态还是动态是按路由段推导的,取决于你有没有用到动态 API、fetch 怎么配的缓存。
  • framework chunk 不建了。 就是上面那行 layer 判断,注释直说了是为 App Router 加的。

最容易踩的是缓存语义,因为它在版本之间反过一次。Next 13 / 14 里 fetch 默认是缓存的(文档原话是默认 force-cache);Next 15 起 fetch 默认不再缓存,要缓存得自己传 cache: 'force-cache'。同一批变更里,Route Handlers 的 GET 也不再默认缓存,客户端路由缓存对页面段的复用也收紧了。照着 14 的教程在 15 上写,症状是数据比预期新、请求比预期多,而没有任何报错。

一句提醒:Next 15 的文档没有把新默认值叫 no-store,它叫 auto no cache,两者在「有没有用动态 API」这件事上的行为并不一样。要精确表述就说「默认不再缓存」。

Pages Router 这套渲染流程本身怎么走 —— 请求进来之后哪一步取数、哪一步 renderToString、客户端靠什么接上 —— 在理解 Next.js 的 SSR , SSG 实现那篇里拆得更细。至于这里的 splitChunks 配置底下 webpack 到底怎么把模块摊成 chunk,见 Webpack 5 打包原理详解

这篇归在 React 与生态下。同一主题里最近的另外两篇是 Jotai v2:React 状态管理的新篇章React 18 完结:最佳实践与注意事项