Next.js Turbopack 模块拆分与 Tree Shaking 分析器解析:以 combined-export 测试快照为例 Next.js Turbopack 模块拆分与 Tree Shaking 分析器解析以 combined-export 测试快照为例【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Next.js 仓库中 Turbopack 的 ECMAScript 模块拆分测试快照turbopack-ecmascript/tests/tree-shaker/analyzer/combined-export/output.md为主体完整解读 Turbopack 如何将一个包含合并导出combined export的 ES 模块切分为多个可独立加载的 Part包括 Item 提取、四阶段依赖图构建、入口点Entrypoints映射以及最终生成的__TURBOPACK_PART__/__TURBOPACK_VAR__导入属性代码。读完本文你可以掌握该分析器的完整工作流程并能读懂 Turbopack 模块拆分测试的快照输出格式。1. 文档定位这是一份期望输出测试快照该文件并不是普通文档而是 Turbopack ECMAScript 模块拆分module fragments / tree shaker测试套件中的一个快照期望输出。它的输入是同目录下的 input.jsconst a a; const b b; export { a, b };测试用例位于 tests.rs 中通过 swc 的#[fixture]宏对所有匹配tests/tree-shaker/analyzer/**/input.js的文件自动建立测试#[fixture(tests/tree-shaker/analyzer/**/input.js)] fn test_fixture(input: PathBuf) { run(input); }运行流程见 tests.rs读取输入并可选解析同目录config.json其中exports字段用于测试启用某几个导出的场景该用例未提供 config.json见 TestConfig用 swc 将输入解析为ModuleAST并执行resolver变换区分未解析标识符与顶层作用域标识符unresolved_mark/top_level_mark调用DepGraph::init提取 Item然后依次执行hoist_vars_and_bindings、evaluate_immediate、evaluate_eventual、handle_exports、handle_explicit_deps并在每个阶段后渲染 mermaid 依赖图通过split_module生成各 Part 与 Entrypoints再用Merger合并模块求值部分将全部结果写入NormalizedOutput并与output.md做快照比对见 compare_to_file。也就是说output.md的每一个章节Items / Phase 1~4 / Final / Entrypoints / Modules (dev|prod)都是分析器管线各阶段的可复现产物。2. Items 阶段把语句切分为 4 个分析单元快照的第一节记录了DepGraph::init的产出# Items Count: 4两个const声明各自成为一个VarDeclaratorItem而合并导出语句export { a, b };被展开成两个 Export 组 ItemItem 3 与 Item 4分别标注export a、export b这也是该用例名为 combined-export 的原因——一条导出语句导出了多个本地绑定分析器必须把它们拆到各自的导出粒度上Item 1:const a a;— Declares:aWrite:aItem 2:const b b;— Declares:bWrite:b每个 Item 的元数据var_decls、read_vars、write_vars、eventual_read_vars、eventual_write_vars、side_effects等由 tests.rs 逐字段打印。本用例中两个声明均无读取也无副作用标记因此没有- Side effects、- Hoisted或- Reads行。Export 组的 Item 在DepGraph::init中以ItemId::Group(ItemIdGroupKind::Export(local, name))形式生成见 graph.rsrender_item_id将其渲染为图中的export a/export b标签tests.rs。3. Phase 1~4依赖图的逐步演化分析器主流程在 mod.rs 的Analyzer::analyze中串联五个阶段测试中每个阶段后各渲染一次 mermaid 图tests.rs。快照中的图依次为Phase 1变量与绑定提升hoist_vars_and_bindings对应 hoist_vars_and_bindings。该阶段收集所有eventual_read_vars/eventual_write_vars延迟读写通常来自闭包等场景并为首个声明位置记录VarState.declarator。本用例中a、b是const且无跨作用域延迟读写因此四个 Item 之间尚无任何边。Phase 2立即求值evaluate_immediate对应 evaluate_immediate。核心规则是某个 Item 若读取了变量则对该变量的last_writes建立强依赖若写入了变量则对last_reads建立弱依赖mermaid 图中以-..-表示本用例没有出现。从快照看两条边在 Phase 2 就已出现export a强依赖const a声明Item3 -- Item1export b强依赖const bItem4 -- Item2。从源码结构看Export 组 Item 在init阶段即记录了导出绑定evaluate_immediate按读取依赖最后写入的通用规则把强依赖补全const声明不会被再次改写源码注释也指明constwrite 意味着后续不再变化因此last_writes中的候选就是唯一的声明 Item 本身。Phase 3延迟求值evaluate_eventual对应 evaluate_eventual对eventual_read_vars补强依赖、对eventual_write_vars补弱依赖。本用例两个变量都不存在延迟读写图与 Phase 2 完全一致——快照中 Phase 3 与 Phase 2 逐行相同正是这一点的直接证据。Phase 4导出处理handle_exports对应 handle_exports。该阶段做两件事把最后一个副作用 Item 标记为is_module_evaluation本用例没有任何副作用语句故不产生模块求值依赖为每个ItemId::Group(ItemIdGroupKind::Export(local, _))向该变量的last_writes添加强依赖见 mod.rs。由于 Phase 2 已建好边此阶段图再次保持不变。之后handle_explicit_depsmod.rs处理用户显式标注的依赖本用例同样无增量。4. FinalCondense 后的最小分组依赖图定稿后经finalize收缩condense快照给出两个等价类节点即{const a声明 export a组} 归为一组{const b声明 export b组} 归为另一组两组之间没有任何依赖。这正是模块拆分的理论最优结果a与b可以独立成两个 Part互不牵连消费方按需加载其中一个即可。5. EntrypointsKey 到 Part 索引的映射快照中 dev 与 prod 两节的入口点完全一致{ ModuleEvaluation: 3, Export( a, ): 0, Export( b, ): 1, Exports: 2, }其含义是Key 到模块 Part 索引的FxHashMapKey, u32见 SplitResult。Key枚举定义在 mod.rsModuleEvaluation模块求值、Export(name)单个导出、Exports全部导出、StarExportsexport *。运行时 get_part_id 负责把ModulePart转换回这里的索引并带有兜底若找不到指定导出会回退到StarExports或ExportsPart注释说明这是为export * from foo服务的仍找不到时则会 dump 所有模块代码并报错方便排查。本例中Export(a) → 0、Export(b) → 1Exports → 2承载两个具名转发的聚合 PartModuleEvaluation → 3空壳。6. 生成的 Part 代码dev 与 prodsplit_module对同一张图分别以 Development 与 Production 两种Mode处理弱依赖后输出tests.rs 中describe先以is_debug true再false各跑一遍。本用例两种模式下快照一致Part 0~3 如下。Part 0—— 导出a的最小单元const a a; export { a }; export { a as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };Part 1—— 导出b的最小单元const b b; export { b }; export { b as b } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };Part 2——Exports聚合把两个具名导出分别转发到对应 Partexport { a } from __TURBOPACK_PART__ assert { __turbopack_part__: export a }; export { b } from __TURBOPACK_PART__ assert { __turbopack_part__: export b };Part 3—— 模块求值壳本模块无副作用语句内容为空export { };这里的__TURBOPACK_PART__是一个保留导入来源常量定义在 mod.rspub(crate) const TURBOPACK_PART_IMPORT_SOURCE: str __TURBOPACK_PART__;导入属性import attribute中的__turbopack_part__/__turbopack_var__值会被 Turbopack 构建期识别辅助函数find_turbopack_part_id_in_asserts、create_turbopack_part_id_assert见 mod.rs 顶部重导出 指向 graph.rs从而把从虚拟模块按 part id 引入翻译成真正的 Part 间依赖。测试代码中代码生成显式启用了with_emit_assert_for_import_attributes(true)tests.rs因此输出里能看到assert { ... }语法——这是快照呈现形式的来源。Merged (module eval)一节展示Merger合并模块求值入口后的结果export { };即模块求值入口合并后依然为空——两个const声明没有任何副作用全部被推迟到各导出 Part 中只有消费方真正 import 时才执行。7. 复现与验证方式查看输入与期望快照input.js、output.md核心实现Analyzer 及四阶段方法、DepGraph、split_modulemod.rs注意其中对 CJS 文件与should_skip_tree_shaking判断的直接跳过逻辑即非标准 ESM 模块不会进入拆分流程在turbopack工作区中对turbopack-ecmascriptcrate 运行cargo testfixture 名test_fixture即可执行全部 analyzer 快照测试输出与output.md不一致时测试失败这正是该快照文件的用途——锁定分析器在合并导出这一典型场景下的确定性行为。8. 小结这份combined-export快照完整展示了一条从源码到 Part 代码的推导链一条export { a, b }语句被展开为两个导出组 Item → 四阶段分析只产生两条导出指向声明的强依赖 → condense 后得到两个互不依赖的分组 → 最终拆出 Part 0/1各含一个const声明、Part 2转发聚合、Part 3空求值壳并用__TURBOPACK_PART__导入属性在 Part 间重新织入链接。理解了这份文档也就掌握了 Turbopack 模块级 tree shaking 测试快照的阅读方法以及合并导出如何影响模块拆分粒度这一核心问题的答案。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考