解读 Turbopack Tree-Shaker 快照 `failed-2`:Next.js dynamic-rendering 模块的 Part 拆分全过程

解读 Turbopack Tree-Shaker 快照 `failed-2`:Next.js dynamic-rendering 模块的 Part 拆分全过程 解读 Turbopack Tree-Shaker 快照failed-2Next.js dynamic-rendering 模块的 Part 拆分全过程【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文基于仓库中的测试快照 output.md对应输入 input.js完整解读 Turbopack 的 ECMAScript 模块拆分器tree-shaker/analyzer是如何把一个真实模块——Next.js 服务端的dynamic-rendering源码——逐语句建档、分四个阶段构建依赖图最终切分成若干__TURBOPACK_PART__分片Part的。读完本文你能看懂这类output.md快照的每个章节Items、Phase 1–4、Final、Entrypoints、Modules dev/prod并理解 dev 与 prod 产物差异的底层原因。一、快照是什么一个由 Rust 测试自动生成的 fixture 输出failed-2是turbopack-ecmascriptcrate 中 tree-shaker 分析器测试集下的一个回归用例目录。测试入口在 tests.rs#[fixture(tests/tree-shaker/analyzer/**/input.js)] fn test_fixture(input: PathBuf) { run(input); }#[fixture]宏会为每个匹配到的input.js生成一个测试测试运行时用 SWC 解析input.js为 AST并执行 resolvertests.rs调用DepGraph::init把模块拆成“条目item”并初始化依赖图依次执行分析器五个步骤hoist_vars_and_bindings→evaluate_immediate→evaluate_eventual→handle_exports→handle_explicit_deps在每步之后渲染一张 mermaid 依赖图tests.rs对 dev / prod 两种模式分别执行handle_weaksplit_module把模块切成 Part并输出 Entrypoints 映射与各 Part 的代码将全部结果与同目录的output.md做快照比对tests.rs。也就是说output.md不是人工撰写的文档而是分析器在该输入上的确定性完整执行记录——这正是它的价值它同时展示了输入源码、中间图状态和最终产物。二、输入模块Next.js 的 dynamic-rendering 源码input.js的内容与 Next.js 仓库中服务端文件 dynamic-rendering.ts 一致从导入路径../../client/components/hooks-server-context、../../lib/url等相对位置可以印证。它提供 PPR/静态渲染场景下的“动态 API 跟踪”能力导出了 8 个函数导出作用从源码行为看createPrerenderState创建预渲染状态dynamicAccesses列表 调试开关markCurrentScopeAsDynamic当前作用域触发了动态 API 时的降级/报错处理trackDynamicDataAccessed跟踪动态数据访问含unstable_cache场景的直接报错Postpone组件形式的 postpone 触发器trackDynamicFetch预渲染期间 fetch 触发 postponeusedDynamicAPIs判断是否发生过动态 API 访问formatDynamicAPIAccesses格式化调试用的调用栈过滤node_modules/next/等噪音行createPostponedAbortSignal把 React 抛出的 postpone 对象转成AbortSignal另有 2 个模块内部函数postponeWithTracking、assertPostpone和 1 个顶层常量hasPostpone它们不导出但被多个导出函数引用——这是后文“模块求值 Part”存在的根源。三、Items分析器为每条语句建的“档案”快照开头的# Items章节Count: 27列出了 19 条语句条目 8 个导出分组节点。每个条目带四类元数据直接对应graph.rs中ItemData的字段Hoisted条目是否被提升import、函数声明都会被提升Side effects条目求值是否可能产生副作用import语句与顶层const求值都算Declares条目声明的绑定如React、DynamicServerError、hasPostponeReads/Write/Reads (eventual)/Write (eventual)条目直接同步执行时读写的变量以及最终函数体执行时才会读写的变量。以const hasPostpone为例Item 9const hasPostpone typeof React.unstable_postpone function;它的档案是- Side effects / Declares: hasPostpone / Reads: React / Write: React, hasPostpone。注意Write: React——读取React.unstable_postpone属性被标记为对React的“潜在写”属性可能被赋值这正是后文postponeWithTracking条目里出现Write (eventual): React的同一机制。而markCurrentScopeAsDynamicItem 11的档案里写的是Reads (eventual): getPathname, StaticGenBailoutError, postponeWithTracking, DynamicServerError这些引用位于函数体内同步执行函数声明本身并不会读它们只有将来调用函数时才会读——direct/eventual 的区分是整个分析器正确性设计的核心。四、四个阶段依赖图如何一步步成形快照中 Phase 1–4 各有一张 mermaid 图。边的语义由 tests.rs 的渲染函数 决定实线箭头--是强依赖虚线箭头-.-是弱依赖。各阶段对应分析器源码 mod.rs 中analyze的固定调用顺序。Phase 1提升变量与绑定hoist_vars_and_bindingshoist_vars_and_bindings 只做两件事把提升项import 语句、函数声明记入各变量的last_writes把“有副作用的提升项”按出现顺序串成链。这解释了 Phase 1 图中仅有的 3 条边Item2 -- Item1; // react 的 ImportBinding 依赖其 ImportOfModule Item3 -- Item2; // hooks-server-context 的 import 语句依赖前一个副作用项 Item4 -- Item3; // 静态生成 bailout 的 import 语句继续串行import 语句必须保持顺序执行因此全部串联在“模块求值链”上4 个ImportBindingItem 2/4/6/8各自依赖对应的 import 语句Item 1/3/5/7 的反向边在render_graph中表现为 Item2→Item1 等。Phase 2即时求值evaluate_immediateevaluate_immediate 处理非提升条目。对唯一带副作用的顶层语句hasPostponeItem 9规则是强依赖上一个副作用项last_side_effects.last()并对每个“最终会读到的变量”在last_writes上建弱依赖、在last_reads上建强依赖。快照中 Item 9 的出边完整体现了这一点Item9 -- Item5; Item9 -- Item4; Item9 -.- Item8; Item9 -.- Item7; Item9 -.- Item15; Item9 -.- Item6; Item9 -.- Item18;虚线指向的 Item 15postponeWithTracking与 Item 18assertPostpone是函数声明它们在 Phase 1 就已被当作提升项写入last_writes所以此刻即产生弱依赖边。同时 Phase 2 还出现了 8 条Item20 -- Item10…Item27 -- Item19的边——每个导出分组节点已经依赖于对应函数的声明条目。Phase 3最终求值evaluate_eventualevaluate_eventual 把每个条目的eventual_read_vars落实为对“最后写者”和“声明者”的强依赖。以markCurrentScopeAsDynamicItem 11为例它最终读取getPathname、StaticGenBailoutError、postponeWithTracking、DynamicServerError四个变量于是新增Item11 -- Item8; // getPathname 的 ImportBinding Item11 -- Item7; // getPathname 的 ImportOfModule Item11 -- Item15; // postponeWithTracking 声明 Item11 -- Item6; // DynamicServerError 的 ImportBindingtrackDynamicDataAccessedItem 12、PostponeItem 13、trackDynamicFetchItem 14、createPostponedAbortSignalItem 19同理。Phase 4 的handle_exports源码则把最后一个副作用条目标记为模块求值锚点并让导出分组强依赖导出变量的最后写者——从快照看 Phase 4 与 Phase 3 的边集相同说明该模块的导出依赖在前序阶段已全部建立。五、Final同依赖条目被合并成组# Final章节输出的是finalize之后的内部化图依赖集合相同的条目合并为一个节点。这张图直接预示了 Part 的划分N8[Items: [ItemId(4, VarDeclarator(0)), ItemId(10, Normal), ItemId(13, Normal)]]; N9[Items: [ItemId(5, Normal), ItemId(Export(createPrerenderState, #2), createPrerenderState)]]; N10[Items: [ItemId(6, Normal), ItemId(Export(markCurrentScopeAsDynamic, #2), markCurrentScopeAsDynamic)]]; ...其中N8把hasPostponeVarDeclaratorpostponeWithTrackingassertPostpone三条目合成一组——它们共享同一依赖集react模块 import、part 0 等将共同构成“模块求值 Part”N9–N16每个导出函数与其导出分组节点合为一组各成一个 Part导入侧的条目也按依赖合并N0/N1 是react的 import 语句与绑定N2/N4/N6 是三个相对模块 import 语句N3/N5/N7 是对应绑定。边仍然区分强弱例如N8 -.- N7、N8 -.- N5是hasPostpone带来的弱依赖而N16 -- N1createPostponedAbortSignal所在组依赖React绑定是强依赖。六、Entrypoints每个入口指向哪个 Partsplit_modulegraph.rs把分组映射为 Part 序号并生成入口表。两种模式的入口如下快照# Entrypoints章节// dev { ModuleEvaluation: 8, Export(Postpone): 12, Export(createPostponedAbortSignal): 16, Export(createPrerenderState): 9, Export(formatDynamicAPIAccesses): 15, Export(markCurrentScopeAsDynamic): 10, Export(trackDynamicDataAccessed): 11, Export(trackDynamicFetch): 13, Export(usedDynamicAPIs): 14, Exports: 17 } // prod { ModuleEvaluation: 8, Export(Postpone): 12, Export(createPostponedAbortSignal): 18, ... Exports: 19 }入口键的类型定义在 mod.rs 的Key枚举ModuleEvaluation/Export(name)/Exports/StarExports消费方按需取用“模块求值 Part”或“某个导出的 Part”而不是整个模块——这就是 Turbopack 对 ESM 模块做部分求值partial evaluation的机制基础。七、Modules 章节dev 产物如何被切成 18 个 Part# Modules (dev)列出了 18 个 Part0–17。它们的结构非常规整Part 0–7import 分片。Part 0 只有import reactPart 2 在依赖 Part 0 的基础上追加import ../../client/components/hooks-server-contextPart 4、6 依次对应另外两个模块。奇数 Part1/3/5/7不携带任何真实 import只import __TURBOPACK_PART__指向其前驱——它们是ImportBinding条目独立成 Part 的结果绑定值从对应 import 分片“接过来”。Part 8模块求值 Part。包含hasPostpone、postponeWithTracking、assertPostpone并通过__TURBOPACK_VAR__以单字母名对外暴露共享变量export { hasPostpone as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true }; export { postponeWithTracking as b } from __TURBOPACK_VAR__ assert { __turbopack_part__: true };__turbopack_var__: true标记“这是跨 Part 变量导出”与指向其他 Part 的__turbopack_part__导入相配合。Part 9–16每个导出函数一个 Part。例如 Part 10 中markCurrentScopeAsDynamic通过{ b as postponeWithTracking } from __TURBOPACK_PART__ assert { __turbopack_part__: -8 }从 Part 8 取共享函数负数 part 编号从输出结构看是指向目标 Part 的符号编码正数出现在 Part 声明前驱依赖的位置。每个导出 Part 同时输出具名export与__TURBOPACK_VAR__字母别名a–k供不同消费方向取用。Part 17Exports 门面。把 8 个具名导出以字符串形式的 part 标识重新导出如__turbopack_part__: export createPrerenderState对应入口表中的Exports: 17。## Merged (module eval)则展示了Merger对模块求值入口的合并结果即 Part 8 的代码体见 tests.rs 中“按入口收集 body 再递归合并”的流程。八、prod 为什么多两个 Part弱依赖只在生产模式被剔除# Modules (prod)有 20 个 Part0–19与 dev 的差异全部来自一个函数——handle_weak/// Weak imports are imports only if it is referenced strongly. But this /// is production-only, and weak dependencies are treated as strong /// dependencies in development mode. pub(super) fn handle_weak(mut self, mode: Mode) { if !matches!(mode, Mode::Production) { return; } // 删除所有 Dependency::Weak 边 }dev 模式保留全部弱边hasPostpone、postponeWithTracking、assertPostpone三者依赖集合一致被合并进同一个 Part 8prod 模式删掉弱边后三者的依赖集不再相同于是各自独立成 PartPart 8prod只剩const hasPostpone typeof React.unstable_postpone function;与export { hasPostpone as a }Part 14postponeWithTracking字母别名c它导入reactPart 0、{ h as assertPostpone } from __TURBOPACK_PART__ assert { __turbopack_part__: -17 }以及 Part 8Part 17assertPostpone别名h导入{ a as hasPostpone } from ... __turbopack_part__: -8消费方编号全部顺延createPostponedAbortSignal从 dev 的 Part 16 变为 Part 18Exports 门面从 17 变为 19与 prod 入口表完全对应。这一对 dev/prod 快照共同验证了弱依赖机制的实际效果开发态宁可多合并少切分、调试友好生产态把仅在“将来可能被用到”时才需要的边剪掉让每个 Part 只携带真正强依赖的最小代码。九、如何复现这份快照在仓库根目录下可以运行该 crate 的 fixture 测试来重新生成并比对这份输出fixture 宏按路径生成测试名failed-2对应用例名cargo test -p turbopack-ecmascript测试逻辑与快照格式都由 tests.rs 定义输入只有input.js可选config.json指定额外导出入口本用例没有输出即为output.md的固定结构# Items→# Phase 1..4→# Final→ dev/prod 各自的# Entrypoints与# Modules。同目录下还有failed-1、failed-3等兄弟用例以及effects-1、ipc-index、node-fetch等覆盖不同场景的 fixture适合横向对照分析器行为。十、小结这份failed-2快照把一个真实业务模块Next.js 的 dynamic-rendering在 Turbopack tree-shaker 中的完整生命周期压缩进一个文件27 个条目的档案提升/副作用/直接读写/最终读写、四阶段依赖图的增量演化、Final 分组、dev/prod 双模式入口表以及 18 与 20 个 Part 的具体代码。它展示了三个可迁移的结论其一直接依赖与最终依赖的分离是函数体内引用能被正确跨 Part 引用的关键其二弱依赖只在生产模式剔除直接造成 dev 与 prod 的 Part 切分差异其三模块被切成“import 分片 / 模块求值 Part / 每导出一个 Part / Exports 门面”四类结构通过__TURBOPACK_PART__与__TURBOPACK_VAR__两组 import assertion 接线从而实现按入口ModuleEvaluation或某个Export(name)的部分加载。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考