Read OSS

构建流水线:webpack 配置、代码生成与三编译器架构

高级

前置知识

  • 第 1-2 篇:架构概览与服务器启动流程
  • Webpack 核心概念——loader、plugin、编译阶段、模块图
  • 理解模块解析与代码分割
  • 熟悉 React Server Components 的服务端/客户端边界

构建流水线:webpack 配置、代码生成与三编译器架构

执行 next build 会驱动整个 JavaScript 生态中最复杂的构建流水线之一。它从文件系统中发现路由,生成将框架基础设施与用户组件连接起来的入口代码,依次运行三个独立的 webpack 编译过程(client、server、edge),产出十余个 manifest 文件,并可选地对所有静态页面进行预渲染。本文将完整梳理这一流程。

构建编排:build/index.ts

构建的入口是 build/index.ts,这个约 4,330 行的文件负责编排整个构建过程。整体流程如下:

flowchart TD
    Start["next build"] --> Config["Load & Validate Config"]
    Config --> Env["Load .env files"]
    Env --> FindPages["Find pages/ and app/ directories"]
    FindPages --> CustomRoutes["Load custom routes\n(rewrites, redirects, headers)"]
    CustomRoutes --> Entries["Collect Entrypoints\n(entries.ts)"]
    Entries --> Compile["Run Webpack Compilation\n(3 compilers)"]
    Compile --> Manifests["Generate Manifests\n(pages, routes, prerender, etc.)"]
    Manifests --> StaticGen["Static Generation\n(prerender pages)"]
    StaticGen --> Export["Export static files"]
    Export --> Output["Write build output\n(.next/)"]

构建从加载配置开始(即第 1 篇介绍过的 loadConfig()),随后发现 pages/app/ 目录。接着,它从 next.config.js 的 rewrites/redirects/headers 中加载自定义路由,从文件系统收集所有入口点,最终启动 webpack 编译。

编译完成后,构建进入静态生成阶段——对每个可预渲染的页面执行渲染,产出 HTML 和 RSC 数据。ISR 页面的初始缓存条目,以及 PPR 页面的静态 shell,都在这一阶段生成。

入口收集与代码生成

entries.ts 模块负责从文件系统发现路由,并将其映射为 webpack 入口点。对于每个路由,它通过构建模板生成入口代码——这是一套在构建时完成、无需运行时反射的代码生成机制。

flowchart LR
    FS["Filesystem\n(app/dashboard/page.tsx)"] --> Entries["entries.ts\n(route discovery)"]
    Entries --> Template["Build Template\n(app-page.ts)"]
    Template --> EntryCode["Generated Entry\n(imports user code +\nframework wrappers)"]
    EntryCode --> Webpack["Webpack Entry"]

构建模板位于 build/templates/

模板 用途
app-page.ts AppPageRouteModule 包装 App Router 页面组件
app-route.ts AppRouteRouteModule 包装 App Router 路由处理器
pages.ts 用 SSR/SSG 基础设施包装 Pages Router 页面
pages-api.ts 包装 Pages Router API 路由
middleware.ts 用 edge runtime 适配器包装 middleware
edge-ssr-app.ts App Router 的 edge runtime SSR

app-page.ts 模板尤为值得关注。它导入 AppPageRouteModule(第 3 篇介绍的类)、用户的 loaderTree,以及 vendor 化的 React 实例,并将它们串联在一起。模板使用 with { 'turbopack-transition': 'next-ssr' } 这样的 import attributes,确保模块以正确的 bundler 配置加载到目标环境中。

代码生成是一个关键的设计决策。Next.js 不依赖运行时反射(即在请求时执行 require(userPagePath)),而是在构建时生成静态 import。这样做的好处是:bundler 能够精确分析每个入口点的依赖关系,从而支持死代码消除、tree shaking 和完整的模块图分析。

提示: 排查构建问题时,可以查看 .next/server/app/[route]/page.js 中生成的入口代码,它清晰地展示了模板是如何将你的页面组件与框架连接起来的。

Webpack 配置工厂

webpack-config.ts 工厂函数(约 2,930 行)会生成三套独立的 webpack 配置,分别对应第 1 篇介绍的三个编译器目标:

flowchart TD
    Factory["webpack-config.ts\ngetBaseWebpackConfig()"] --> Client["Client Config"]
    Factory --> Server["Server Config"]
    Factory --> Edge["Edge Server Config"]

    Client --> ClientBundle["Browser JavaScript\n- React client components\n- Client-side router\n- CSS/assets"]

    Server --> ServerBundle["Node.js Bundle\n- RSC rendering\n- API routes\n- SSR"]

    Edge --> EdgeBundle["Edge Bundle\n- Middleware\n- Edge routes\n- No Node.js APIs"]

    subgraph "Key Differences"
        ClientDiff["Client:\n- target: 'web'\n- externals: none\n- splits code by page"]
        ServerDiff["Server:\n- target: 'node'\n- externals: node_modules\n- bundled React"]
        EdgeDiff["Edge:\n- target: 'webworker'\n- polyfilled APIs\n- size-constrained"]
    end

三套配置共享一个公共基础,但在以下方面存在显著差异:

  • Target'web'(client)、'node'(server)、'webworker'(edge)
  • Externals:server 编译器将 node_modules 设为外部依赖(运行时可直接访问),client 编译器则将所有依赖打包进 bundle。edge 编译器采取选择性打包策略,并对不可用的 Node.js API 进行 polyfill。
  • 模块解析:server 编译器使用 react-server condition 解析 RSC 模块,client 编译器使用标准的 browser condition。'use client' 边界正是以此为基础——同一个包根据编译器目标解析到不同的代码。
  • Webpack layers:server 编译器通过 layers(WEBPACK_LAYERS)隔离 RSC 代码与 SSR 代码,防止 server-only 的 import 泄漏到 client bundle 中。

配置中同样包含优化策略——client 端的代码分割、server 端的 chunk 策略,以及 CSS 提取。开发模式下,还会通过 ReactRefreshWebpackPlugin 启用 React Fast Refresh。

关键 Webpack Plugin:Flight Manifest 与 Client Entry

有两个 plugin 对于 React Server Components 跨服务端/客户端边界工作至关重要:

FlightManifestPlugin

flight-manifest-plugin.ts 生成 Client Reference Manifest,告诉服务端如何引用客户端组件。Server Component 渲染客户端组件时,并不直接渲染其代码,而是输出一个引用。这份 manifest 将这些引用映射到实际的客户端 chunk 文件。

flowchart LR
    ServerComp["Server Component\n(renders <ClientButton />)"] --> Ref["Client Reference\n{id: 'Button', chunks: [...]}"]
    Ref --> Manifest["Client Reference Manifest\n(flight-manifest.json)"]
    Manifest --> ClientChunk["Client Chunk\n(Button.js)"]

    subgraph "Server Bundle"
        ServerComp
        Ref
    end

    subgraph "Client Bundle"
        ClientChunk
    end

    Manifest -.->|"Maps references\nto chunks"| ServerComp
    Manifest -.->|"Tells browser\nwhat to load"| ClientChunk

FlightClientEntryPlugin

flight-client-entry-plugin.ts 遍历模块图,找出所有 'use client' 边界。一旦发现带有 'use client' 指令的模块,它就会为该模块及其所有依赖创建客户端入口点。这正是"客户端组件"抽象在 bundler 层面的实现方式——plugin 自动在 'use client' 边界处切分模块图。

此外,该 plugin 还负责处理 Server Actions('use server'),生成 Server Reference Manifest,告知客户端如何调用服务端函数。

这两个 plugin 共同构成了 React Server Components(运行时概念)与 Webpack(构建时工具)之间的桥梁。它们分析模块图以理解服务端/客户端边界,进而生成两端运行时用于跨边界引用代码的 manifest。

App Loader:从文件系统到模块图

next-app-loader 是一个 webpack loader,负责将 app/ 目录的约定式结构转化为渲染引擎所需的 loaderTree 数据结构。对于如下目录结构:

app/
  layout.tsx
  page.tsx
  dashboard/
    layout.tsx
    page.tsx
    loading.tsx
    error.tsx

loader 会生成如下树形结构:

graph TD
    Root["['', { layout: './app/layout.tsx', children: ... }]"] --> Dashboard["['dashboard', { layout: './app/dashboard/layout.tsx',\nloading: './app/dashboard/loading.tsx',\nerror: './app/dashboard/error.tsx',\nchildren: ... }]"]
    Dashboard --> Page["['page', { page: './app/dashboard/page.tsx' }]"]

loader tree 中的每个节点都是 [segment, modules, children] 的三元组。modules 对象包含对约定文件的引用——layout.tsxpage.tsxloading.tsxerror.tsxtemplate.tsxnot-found.tsx。这一结构被序列化进 webpack 模块图,随后由 create-component-tree.tsx(第 3 篇)消费,用于构建 React 元素树。

该 loader 同样处理并行路由(以 @ 开头的目录名)、路由分组(括号包裹的目录名)和拦截路由——通过解释文件系统约定并生成相应的树形结构来实现。

提示: 如果你在排查某个 layout 或 error boundary 不生效的问题,不妨直接检查 loader tree。在编译后的 server 产物中搜索 loaderTree,即可找到它。树的结构直接对应组件的嵌套方式。

其他 Bundler:Turbopack 与 Rspack

上述基于 webpack 的流水线是"参考实现"。Turbopack 和 Rspack 作为替代方案,在编译步骤接入:

flowchart TD
    Build["build/index.ts"] --> BundlerCheck{"Which bundler?"}

    BundlerCheck -->|Webpack| WebpackConfig["webpack-config.ts\n(3 compilers)"]
    BundlerCheck -->|Turbopack| TurbopackBuild["turbopack-build/\n(Rust compilation)"]
    BundlerCheck -->|Rspack| RspackBuild["next-rspack/\n(Rspack compilation)"]

    WebpackConfig --> Manifests["Shared Manifest Format"]
    TurbopackBuild --> Manifests
    RspackBuild --> Manifests

    Manifests --> StaticGen["Static Generation\n(shared across bundlers)"]
    Manifests --> ServerRuntime["Server Runtime\n(shared across bundlers)"]

关键在于:三种 bundler 产出相同格式的 manifest。server runtime 不关心 manifest 由哪个 bundler 生成——无论使用何种构建工具,它都统一读取 build-manifest.jsonpages-manifest.jsonclient-reference-manifest.json 等文件。

Turbopack 的构建路径位于 build/turbopack-build/,委托给 crates/next-core 中的 Rust crate 执行。它复用了相同的入口点发现逻辑和代码生成模板,但实际的 bundling 由 Turbopack 的 Rust 引擎完成——得益于增量计算和并行化,速度大幅提升。

Rspack 位于 packages/next-rspack,在 API 层面与 webpack 兼容。它大量复用了 webpack-config.ts 中的配置(Rspack 本就设计为 webpack 的直接替代品),但底层运行的是 Rspack 基于 Rust 的编译器,而非 webpack 的 JavaScript 实现。

三 bundler 并行的策略是务实之举:Turbopack 代表未来(最快、专为 Next.js 打造),webpack 是经过验证的现在(生态兼容性最佳),Rspack 则是两者之间的务实选择(速度快,兼容 webpack)。统一的 manifest 格式,是这一多 bundler 策略得以实现而无需复制 server runtime 的根本保障。

下一步

我们已经完整梳理了从文件系统发现、代码生成、编译,到 manifest 输出的整个构建流水线。在最后一篇文章中,我们将深入探讨贯穿整个系统的缓存架构——从对并发请求去重的响应缓存,到持久化 ISR 页面的增量缓存,再到 use cache 指令的缓存处理器接口,以及基于 tag 的重新验证机制。