Next.js Turbopack 模块切分快照解析:以 grouping 用例理解 tree-shaker 分析器的 Item 分解、依赖图阶段与 Part 切分 📅 发布时间:2026/9/8 21:32:23 👁 浏览次数: Next.js Turbopack 模块切分快照解析以 grouping 用例理解 tree-shaker 分析器的 Item 分解、依赖图阶段与 Part 切分【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Next.js 仓库中 Turbopack 的 tree-shaker 测试快照 grouping/output.md 为主体结合其输入文件 grouping/input.js 与测试驱动源码 tests.rs完整还原一条“从 14 行源码到多个可独立加载的 Part 模块”的切分过程。读完后你将理解 Turbopack 如何将模块顶层语句分解为带读/写变量标注的 Item、如何经过 Phase 1–4 逐步构建依赖图以及split_module最终如何利用__TURBOPACK_PART__/__TURBOPACK_VAR__占位导入把切分结果接线成可运行的模块碎片并能自行对照仓库验证 dev 与 prod 两种模式的切分差异。1. 快照的来源一个 fixture 驱动的测试用例这个output.md并不是手写文档而是测试夹具fixture的期望输出快照。测试入口定义在 tests.rs#[fixture(tests/tree-shaker/analyzer/**/input.js)] fn test_fixture(input: PathBuf) { run(input); }任何tests/tree-shaker/analyzer/子目录下的input.js都会成为一个用例本用例目录为 grouping/。run()的执行流程见 tests.rs为用 swc 解析input.js开启 JSX并运行 resolver 标记顶层作用域调用DepGraph::init把模块拆解为若干 Item逐条写出# Items段源码打印Declares/Reads/Write/Side effects等元信息对应 tests.rs依次执行分析器各阶段每步渲染一次 Mermaid 依赖图hoist_vars_and_bindingsPhase 1→evaluate_immediatePhase 2→evaluate_eventualPhase 3→handle_exportsPhase 4→handle_explicit_depsfinalizeFinal见 tests.rs分别以 Development 与 Production 两种模式调用g.split_module打印各 Part 源码与Merged结果见 tests.rs将整份报告通过NormalizedOutput::from(s).compare_to_file(input.with_file_name(output.md))与快照比对tests.rs。本用例没有config.json因此默认exports: []只测试ModuleEvaluation这一个入口的合并tests.rs。1.1 输入代码完整的输入仅 14 行input.jslet x 1; x 2; x 3; console.log(x); x 4; x 5; x 6; x 7; x 8; x 9; export { x }; export const y x;它刻意构造了一个“对同一变量反复写入、中间夹一个副作用读取、最后两个导出”的模式用来考察分析器如何给一连串赋值语句建立顺序依赖。2. Items顶层语句被拆成了 13 个 Item快照第一部分是# ItemsCount: 13。这 13 个节点中 11 个是真实语句 Item另外 2 个是导出分组节点ItemId::Group打印时被ItemId::Group(_) continue跳过所以在## Item N列表中只出现 11 条。Item 的类型与分组类型定义在 graph.rspub(crate) enum ItemId { Item { index: usize, kind: ItemIdItemKind }, Group(ItemIdGroupKind), } pub(crate) enum ItemIdItemKind { Normal, ImportOfModule, ImportBinding(u32), ReexportBinding(u32), VarDeclarator(u32), }快照中的全部 Item 及其标注如下编号对应stmt 序号与input.js的行一一对应Item语句类型DeclaresReadsWriteSide effects1let x 1;VarDeclarator(0)xx2x 2;Normalx3x 3;Normalx4console.log(x);Normalx有5x 4;Normalx6x 5;Normalx7x 6;Normalxx8x 7;Normalxx9x 8;Normalxx10x 9;Normalxx11export const y x;VarDeclarator(0)yxy另有export { x }语句本身不产生语句 Item而是生成两个导出分组节点在图中记为 Item 12export x与 Item 13export y。这些字段正是 graph.rs 中ItemData结构体的直接体现var_decls声明、read_vars即时读取、write_vars写入副作用、side_effects未知副作用如console.log以及本用例未触发的eventual_read_vars/eventual_write_vars“最终会读/写”用于函数体等需先触发副作用才执行的场景源码注释见 graph.rs。值得注意的一个细节console.log(x)被标记为 Side effects这正是后面它被划入“模块求值”组即必须最先执行的代码的依据——有副作用且被读取的语句不能随无关导出被丢弃。3. Phase 1–4 与 Final依赖图的四次演进快照随后用五张 Mermaid 图记录依赖图的演化。Phase 1 只有节点、没有边是hoist_vars_and_bindings之后的初始状态evaluate_immediate之后Phase 2每个“写”节点连向其依赖来源。实线箭头表示由声明/读取建立的直接依赖虚线箭头是附加在写操作上的顺序约束边。以x 2;为例它写x必须排在let x 1;Item 1之后于是有Item2 -- Item1。完整的 Phase 2 图如下可以观察到几条关键依赖赋值链x 2 → x 3 → … → x 9形成对声明节点 Item 1 的链式依赖Item2 -- Item1、Item7 -- Item6、…、Item10 -- Item9保证运行顺序与原模块一致读取点console.log(x)Item 4依赖它读取时x的最新写入方x 3Item4 -- Item3同时依赖声明Item4 -- Item1导出点export xItem 12依赖最后一次写入x 9Item12 -- Item10与声明export yItem 13依赖export const y x;Item 11后者又依赖x 9Item11 -- Item10——因为y的值取自求值时刻的x。evaluate_eventualPhase 3与handle_exportsPhase 4在本用例中没有新增边本例不存在“最终读/写”函数体等延迟读取导出分组节点及其边在初始化阶段就已入图。因此 Phase 3、Phase 4 的图与 Phase 2逐边完全一致快照原文如此这本身也是快照的价值之一——它锁定了“这些阶段不应产生额外变化”这一行为。finalize之后得到Final图节点先被压缩为若干“组”组内语句相互紧邻、可整体移动边只剩组间关系六个组的语义N0let x 1;—— 变量声明所有使用方的公共依赖N1x 2;—— 独立小组它的后续写入x 3被副作用读取点截断成另一组;N2x 3; console.log(x); x 4;—— 含副作用读取console.log的组是“模块求值”的锚点N3x 5; x 6; x 7; x 8; x 9;—— 纯赋值链可整体移动N4export const y x;export y分组 ——y的定义与其导出同组N5export x分组 —— 单独成组依赖 N3最终值与 N0声明。4. Entrypoints组到 Part 的映射split_modulegraph.rs把 Final 图落盘为若干 Part 模块并返回“入口键 → Part 序号”的映射。快照中出现了两版映射分别来自 Development 与 Production 两次切分测试通过g.handle_weak(if is_debug { Mode::Development } else { Mode::Production })切换见 tests.rs# 切分一次Development 模式的 Entrypoints { ModuleEvaluation: 2, Export(x): 5, Export(y): 4, Exports: 6, }# 切分二次Production 模式的 Entrypoints { ModuleEvaluation: 2, Export(x): 6, Export(y): 5, Exports: 7, }键的含义与 tests.rs 一致ModuleEvaluation指向“必须最先执行的组”这里 N2即console.log所在组Export(x)/Export(y)指向各导出对应的 PartExports指向纯导出转发模块只含export ... from供外部导入方使用。两版映射的序号差 1说明 prod 模式多切出了一个 Part下文 Part 3 的x 4;。5. Modules (dev)Development 模式下的 7 个 PartDevelopment 模式切分结果为 7 个 PartPart 0–6快照原文如下。Part 0声明组 N0并向全局变量表注册xlet x 1; export { x as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };Part 1N1x 2import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 2;Part 2N2模块求值锚点也是ModuleEvaluation入口指向的 Partimport { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 3; console.log(x); x 4; export { };Part 3N3纯赋值链整体import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 5; x 6; x 7; x 8; x 9;Part 4N4y的定义 export yimport { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; import __TURBOPACK_PART__ assert { __turbopack_part__: 3 }; const y x; export { y }; export { y as b } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };Part 5N5export ximport { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; import __TURBOPACK_PART__ assert { __turbopack_part__: 3 }; export { x };Part 6Exports入口纯转发模块export { y } from __TURBOPACK_PART__ assert { __turbopack_part__: export y }; export { x } from __TURBOPACK_PART__ assert { __turbopack_part__: export x };5.1 占位导入的接线规则从这些 Part 可以归纳出切分输出的接线规则跨 Part 共享变量声明x的 Part 0 通过export { x as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true }把变量注册到虚拟模块__TURBOPACK_VAR__其余 Part 用import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }从“Part 0”取回。a是对x的改名结果——split_module内部用 base54 编码器对跨 Part 变量统一改名见 graph.rs 的mangle本例中x → a、y → b。从源码结构看这种“变量注册表”方式让 Part 4/5 能读x而不必反向 import 声明语句所在的真实模块从而在 Part 之间避免循环依赖组间执行顺序Part 4/5 额外有一条import __TURBOPACK_PART__ assert { __turbopack_part__: 3 }副作用导入对应 Final 图中N4 -- N3、N5 -- N3的边确保赋值链Part 3在求值y、读取x导出值之前完成命名导出Part 6 中的字符串值export x/export y指向带导出名的 Part即 Part 5/4使外部对原模块的import { x, y }只需 import 这个稳定的转发模块。5.2 Merged (module eval)测试还会把ModuleEvaluation入口对应的 Part 经Merger合并回去验证合并后的语义等价性tests.rs。Development 模式下入口只有 Part 2 一个合并结果为import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 3; console.log(x); x 4; export { };保留export { }空导出语句是为了维持“这是一个 ES 模块”的语义。6. Modules (prod)Production 模式进一步收紧Production 模式切分出 8 个 PartPart 0–7与 dev 的差别集中在两点Part 0–1与 dev 完全相同。Part 2ModuleEvaluation入口只剩两句x 4被移走import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 3; console.log(x); export { };Part 3新增只含被剥离出来的x 4;import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 4;Part 4原 dev 的 Part 3赋值链import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 5; x 6; x 7; x 8; x 9;Part 5N4y的定义与导出依赖从 Part 3 变为 Part 4import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; import __TURBOPACK_PART__ assert { __turbopack_part__: 4 }; const y x; export { y }; export { y as b } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };Part 6N5export ximport { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; import __TURBOPACK_PART__ assert { __turbopack_part__: 4 }; export { x };Part 7Exports转发模块与 dev 的 Part 6 内容相同只是序号后移export { y } from __TURBOPACK_PART__ assert { __turbopack_part__: export y }; export { x } from __TURBOPACK_PART__ assert { __turbopack_part__: export x };Merged (module eval)prod相应收短为import { a as x } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; x 3; console.log(x); export { };从两套输出可以推断出 prod 与 dev 的差异所在prod 模式下对“弱依赖”的处理handle_weak(Mode::Production)在 tests.rs 中调用把x 4与console.log组的绑定拆开使模块求值入口只保留x 3; console.log(x);两个语句而x 4独立成 Part仅在后续组const y x、export x执行前被副作用导入触发。也就是说prod 模式把“对导出结果无直接影响的写操作”从入口热路径上移了出去让入口 Part 更小、更早可执行。这一行为差异正是该快照文件同时保留 dev/prod 两段输出的意义任何修改handle_weak、finalize或split_module的改动只要让任一侧输出变化快照比对就会失败。7. 如何在仓库中复现与验证快照与输入output.md、input.js测试驱动与报告生成逻辑tests.rsrun()、describe()、SingleModuleLoader与print()图与切分核心graph.rs 中的ItemId/ItemIdGroupKind/ItemData、split_module生产管线入口split_module在构建流程中的调用见 mod.rsasync fn split_module(asset: VcEcmascriptModuleAsset)。验证方式是运行该 crate 的 fixture 测试在turbopack/crates/turbopack-ecmascript下执行cargo test由swc_core::testing的 fixture 宏按tests/tree-shaker/analyzer/**/input.js自动收集用例。若修改了分析器或切分逻辑output.md会随实际输出变化而需要在本地更新快照——这也是阅读这类快照文件时的实用视角它既是文档完整记录了一个最小用例的切分全过程也是回归测试的期望值。8. 小结该快照完整演示了 Turbopack tree-shaker 的四阶段分析流程Item 分解13 节点→ 立即依赖求值 → 最终依赖求值 → 导出处理 → 组化压缩6 组关键结论可从图中直接读出副作用读取console.log(x)会把赋值链截断成“入口组 纯赋值组”导出则依赖最后一次写入split_module的产物是多个 Part通过__TURBOPACK_PART__组间顺序/跨组引用-0表示声明组、数字表示 Part 序号、字符串表示命名导出 Part与__TURBOPACK_VAR__跨 Part 共享变量的改名注册表两类占位导入完成接线dev 与 prod 的切分差异x 4是否并入入口组体现了生产模式对模块求值入口的进一步收紧是理解 Turbopack 模块碎片化加载策略的一个最小而完整的样本。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考