news 2026/9/8 17:09:29

深入 Next.js 中 Turbopack Tree Shaker 分析器:从一份测试快照看懂依赖图构建与模块切分全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 Next.js 中 Turbopack Tree Shaker 分析器:从一份测试快照看懂依赖图构建与模块切分全流程

深入 Next.js 中 Turbopack Tree Shaker 分析器:从一份测试快照看懂依赖图构建与模块切分全流程

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

本文以 Turbopack 的 ECMAScript 模块处理 crate(turbopack-ecmascript)中 tree-shaker 分析器的一个测试快照为对象,完整还原"一段 17 条语句的 ES 模块"是如何被拆解为 17 个分析项、经过提升/立即求值/最终求值/导出处理等阶段构建出强/弱依赖图,再被切分为 13 个模块 Part 并合并出入口的。读完本文,你可以看懂 Turbopack tree-shaking 快照的每一个字段与每一种边,并理解 dev 与 prod 两种模式下弱依赖差异处理的实际效果。

这份文档是什么:一个分析器的全链路快照

关联文档 output.md 并不是人写的说明文档,而是 Turbopack tree-shaker 分析器针对测试用例 1/input.js 自动生成的参考快照。它由 fixture 测试驱动:

  • 测试入口位于 tests.rs,通过#[fixture("tests/tree-shaker/analyzer/**/input.js")]宏扫描整个tests/tree-shaker/analyzer/目录,用例 1 只是其中 38 个用例之一(同目录还有shared-and-side-effectsnext-responsetemplate-pages等针对真实场景的用例)。
  • 每个用例的input.js被 swc 解析为模块 AST(并先运行resolverpass 做变量解析),随后交给分析器;tests.rs 还支持读取同目录下的config.json指定"仅拉取哪些导出"来测试(部分用例如test-config-1附带该配置),用例 1 无配置,走默认全量分析。
  • 最终把每一步的中间产物序列化为 Markdown,用NormalizedOutput::compare_to_fileoutput.md做快照比对(见 tests.rs),因此该文件是测试的期望输出,也是理解分析器行为最完整的"执行日志"。

用例 1 的输入模块只有 18 行,却刻意覆盖了提升(hoisting)、副作用、闭包内的最终读/写、导出分组等所有难点:

// tests/tree-shaker/analyzer/1/input.js import { upper } from "module"; export let foobar = "foo"; export const foo = foobar; const bar = "bar"; foobar += bar; let foobarCopy = foobar; foobar += "foo"; console.log(foobarCopy); foobarCopy += "Unused"; function internal() { return upper(foobar); } export function external1() { return internal() + foobar; } export function external2() { foobar += "."; }

分析器的主流程在 tests.rs 中按固定顺序执行:

  1. g.init(...):遍历 AST,抽取全部分析项(Items);
  2. analyzer.hoist_vars_and_bindings():处理变量/函数声明提升;
  3. analyzer.evaluate_immediate(...):求值顶层立即执行的语句,建立语句间依赖;
  4. analyzer.evaluate_eventual(...):处理提升函数体中的"最终"(调用时才会发生)读写;
  5. analyzer.handle_exports(...):为每个导出建立分组节点;
  6. analyzer.handle_explicit_deps():处理显式依赖;
  7. g.finalize(...):把强连通的项压缩成最终节点;
  8. g.handle_weak(Mode)+g.split_module(...):按 dev/prod 模式弱化弱依赖,并切分出模块 Part 与入口映射。

下面按照快照的章节逐段解读。

第一步:Items——把 AST 语句拆成 17 个分析项

快照开头的 "Items / Count: 17" 段列出了全部 17 个项。每项由ItemId::Item { index, kind }标识(语句在模块体中的序号 + 种类),种类有四种:

  • ImportOfModule:带模块副作用的裸导入(import "module"中必须求值的模块部分);
  • ImportBinding(0):从该模块导入的具体绑定(upper);
  • VarDeclarator(0):顶层变量声明语句(let/const);
  • Normal:普通语句或函数声明。

每个项附带一组属性(字段定义可见 tests.rs 的打印逻辑,结构体位于 module_fragments/mod.rs):

属性含义
Hoisted声明可提升,定义可被延迟到真正使用时加载
Side effects求值有副作用,不可随意删除
Declares声明的顶层绑定
Reads/Reads (eventual)立即读 / 最终读(函数被调用时才发生)
Write/Write (eventual)立即写 / 最终写

17 个项逐一对照输入代码:

#语句种类关键属性
1Stmt 0import { upper } from "module";ImportOfModuleHoisted;Side effects(导入模块本身有求值副作用)
2Stmt 0import { upper } from "module";ImportBinding(0)Hoisted;Declares:upper
3Stmt 1export let foobar = "foo";VarDeclarator(0)Declares:foobar;Write:foobar
4Stmt 2export const foo = foobar;VarDeclarator(0)Declares:foo;Reads:foobar;Write:foo
5Stmt 3const bar = "bar";VarDeclarator(0)Declares:bar;Write:bar
6Stmt 4foobar += bar;NormalReads:bar,foobar;Write:foobar
7Stmt 5let foobarCopy = foobar;VarDeclarator(0)Declares:foobarCopy;Reads:foobar;Write:foobarCopy
8Stmt 6foobar += "foo";NormalReads:foobar;Write:foobar
9Stmt 7console.log(foobarCopy);NormalSide effects;Reads:foobarCopy
10Stmt 8foobarCopy += "Unused";NormalReads:foobarCopy;Write:foobarCopy
11Stmt 9function internal() { ... }NormalHoisted;Declares:internalReads (eventual):upper,foobar;Write:internal
12Stmt 10export function external1() { ... }NormalHoisted;Declares:external1Reads (eventual):internal,foobar;Write:external1
13Stmt 11export function external2() { ... }NormalHoisted;Declares:external2;Write:external2Write (eventual):foobar
14导出分组Group标注为export foobar
15导出分组Group标注为export foo
16导出分组Group标注为export external1
17导出分组Group标注为export external2

三个值得注意的细节:

  1. 一条导入语句被拆成两个项。Item 1(ImportOfModule)代表模块求值的副作用,Item 2(ImportBinding(0))代表对upper这个具体绑定的使用——这正是后续 dev/prod 能做出不同加载策略的基础。
  2. eventual前缀区分"现在发生"与"将来发生"internal函数声明本身立即执行(只是注册一个函数对象),但它对upperfoobar的读取要等函数被调用时才发生,所以标为Reads (eventual);同理external2foobar的写入是Write (eventual)
  3. foofoobar是别名关系const foo = foobar之后两者指向同一绑定,这一点会体现在后文 Item 6 到 Item 4 的弱边(Item6 -.-> Item4)中。

Phase 1–4:依赖图的四次演进

快照的 Phase 1 到 Phase 4 各给出一张 Mermaid 有向图。边分两种:实线A --> B强依赖(Dependency::Strong,B 必须先于 A 执行),虚线A -.-> B弱依赖Dependency::Weak,在满足条件的模式下可被延迟或跳过)。边方向语义为"前者依赖后者",渲染逻辑见 tests.rs。

Phase 1(提升之后):还没有任何边

hoist_vars_and_bindings只标记了哪些项可提升(Item 1、2、11、12、13),语句间的依赖尚未分析,图为 17 个孤立节点。

Phase 2(立即求值之后):顶层执行顺序成边

这一阶段为顶层语句建立了按执行顺序的读写边,并为四个导出分组各连了一条边:

  • Item4 --> Item3const foo = foobar读取foobar的声明;
  • Item6 --> Item5Item6 --> Item3foobar += bar依赖barfoobar的声明;
  • Item6 -.-> Item4:因为foofoobar的别名,对foobar的写与foo之间只是弱关联;
  • Item7 --> Item6Item7 --> Item3let foobarCopy = foobar的读取先于本次+=的结果;
  • Item8 --> Item6Item8 --> Item3Item8 -.-> Item7foobar += "foo"的执行顺序依赖;
  • Item9 --> Item7Item9 --> Item1Item9 --> Item8console.log(foobarCopy)有副作用,必须执行,且要求模块导入(Item 1)已完成、foobarCopy已被赋值;其中Item9 -.-> Item2(对upper绑定的依赖)与Item9 -.-> Item11(可能间接涉及internal)是弱边;
  • Item10 --> Item7Item10 -.-> Item9foobarCopy += "Unused"依赖拷贝值,但对console.log的依赖是弱的;
  • 导出分组边:Item14 --> Item8Item14 --> Item3(导出foobar依赖其全部立即写)、Item15 --> Item4(导出foo)、Item16 --> Item12(导出external1)、Item17 --> Item13(导出external2)。

Phase 3(最终求值之后):提升函数补上"调用时"的边

这是图中变化最大的一步,全部来自三个提升函数的闭包分析:

  • Item11 --> Item2internal调用upper,需要导入绑定;
  • Item11 --> Item8Item11 --> Item3internal最终读取的foobar必须包含foobar += "foo"(Item 8)之后的值,且依赖声明(Item 3);
  • Item12 --> Item11Item12 --> Item8Item12 --> Item3external1依赖internalfoobar的最终状态;
  • Item13 -.-> Item14external2foobar最终写foobar的导出分组之间是弱依赖——导出方拿到的是写入前的绑定;
  • Item13 --> Item3external2依赖foobar的声明。

Phase 4 与 Final:显式依赖与连通压缩

本用例没有显式依赖(handle_explicit_deps无新增边),Phase 4 与 Phase 3 完全相同。随后finalize把强连通的项压缩为 12 个最终节点:

  • N0=[Item(0, ImportOfModule)]N1=[Item(0, ImportBinding(0))](即裸导入与upper绑定被拆成两个独立可加载单元);
  • N4=[Item(3), Item(4)]const barfoobar += bar合并(执行紧邻、强连通);
  • N7=[Item(7), Item(8)]console.logfoobarCopy += "Unused"同节点;
  • N9=[Item(10), Export("external1")]:函数声明与其导出分组合并;
  • N10=[Item(11), Export("external2"), Export("foobar")]external2声明与两个导出分组同节点——因为external2持有对foobar的最终写,导出foobar必须与它共存;
  • N11=[Export("foo")]

最终图中的弱边(N7 -.-> N8N7 -.-> N1N6 -.-> N5N4 -.-> N3)正是后面 prod 模式可以"偷懒"的地方。

Entrypoints:入口键到 Part 的映射

切分完成后,split_module返回SplitModuleResult { modules, entrypoints, .. }(结构定义在 graph.rs),entrypoints是一张"入口键 → Part 下标"的映射,消费方按它按需拉取代码。dev 与 prod 的入口映射分别是:

# dev { ModuleEvaluation: 7, Export("external1"): 9, Export("external2"): 10, Export("foo"): 11, Export("foobar"): 10, Exports: 12 } # prod { ModuleEvaluation: 7, Export("external1"): 9, Export("external2"): 10, Export("foo"): 3, Export("foobar"): 11, Exports: 12 }

解读:

  • ModuleEvaluation: 7表示"模块求值"入口在 Part 7,即模块顶层语句的集合;
  • 每个Export(name)键指向持有该导出的 Part;Exports: 12指向 Part 12 的"导出门面"(统一 re-export 所有导出);
  • 两模式的差异在于foofoobar:dev 下foo的导出被拆到独立的 Part 11、foobar的导出与external2合并在 Part 10;prod 下foo直接由声明它的 Part 3 导出,foobar则被挪到专门的 Part 11。这正是handle_weak(Mode::Production)(见 tests.rs)弱化弱依赖后重新切分的结果。

Modules (dev):13 个 Part 的拆分细节

dev 模式的输出展示了树摇后的代码形态。所有 Part 间引用都通过两条 import assertion 表达:

  • import ... from "__TURBOPACK_PART__" assert { __turbopack_part__: N }:加载第 N 个 Part。正数(如3)表示"第 3 个 Part 的顶层代码",负数(如-2)表示"第 2 个 Part 导出的活绑定变量";
  • export { x as v } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }:把let/const声明导出为跨 Part 的活绑定(其他 Part 通过import { v as x } from ... -2拿到同一变量,赋值依然可见)。字符串形式的值(如"export external1")则指向对应的入口。

关键 Part 逐一说明(其余 Part 遵循相同模式):

// Part 0:裸导入的副作用载体 import "module"; // Part 2:foobar 的声明(活绑定 a) let foobar = "foo"; export { foobar as a } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }; // Part 3:foo 的声明,通过 -2 拿到 foobar 的活绑定 import { a as foobar } from "__TURBOPACK_PART__" assert { __turbopack_part__: -2 }; const foo = foobar; export { foo as b } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }; // Part 4:bar 声明 + foobar += bar,并把 part 3 作为前置依赖拉进来 import { a as foobar } from "__TURBOPACK_PART__" assert { __turbopack_part__: -2 }; import "__TURBOPACK_PART__" assert { __turbopack_part__: 3 }; const bar = "bar"; foobar += bar; export { bar as c } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }; // Part 7(模块求值入口):顶层副作用语句的集合 import { d as foobarCopy } from "__TURBOPACK_PART__" assert { __turbopack_part__: -5 }; import "__TURBOPACK_PART__" assert { __turbopack_part__: 8 }; // 拉入 internal(强依赖) import "__TURBOPACK_PART__" assert { __turbopack_part__: 6 }; // foobar += "foo" import "__TURBOPACK_PART__" assert { __turbopack_part__: 1 }; // upper 绑定包装 import "__TURBOPACK_PART__" assert { __turbopack_part__: 0 }; // import "module" console.log(foobarCopy); foobarCopy += "Unused"; export { }; // Part 9 / Part 10:导出函数,各自携带 Export 分组 function external1() { return internal() + foobar; } export { external1 }; export { external1 as f } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }; function external2() { foobar += "."; } export { external2 }; export { foobar }; export { external2 as g } from "__TURBOPACK_VAR__" assert { __turbopack_var__: true }; // Part 11(dev 下 Export("foo") 入口):从 Part 3 的活绑定 re-export import { b as foo } from "__TURBOPACK_PART__" assert { __turbopack_part__: -3 }; export { foo }; // Part 12(Exports 门面):按入口名字符串统一转发 export { external1 } from "__TURBOPACK_PART__" assert { __turbopack_part__: "export external1" }; export { external2 } from "__TURBOPACK_PART__" assert { __turbopack_part__: "export external2" }; export { foobar } from "__TURBOPACK_PART__" assert { __turbopack_part__: "export foobar" }; export { foo } from "__TURBOPACK_PART__" assert { __turbopack_part__: "export foo" };

最后还有一段 "Merged (module eval)",测试用 Merger(merge_recursively)把ModuleEvaluation入口的 Part 及其强依赖递归合并成一个模块,模拟运行时"执行一次模块顶层"实际会跑到的全部代码:dev 下合并结果为import { d as foobarCopy } from -5+ 拉取 Part 8、6、1、0 +console.log(foobarCopy); foobarCopy += "Unused";

Modules (prod):弱依赖弱化带来的三个关键变化

prod 模式先调用handle_weak(Mode::Production),与 dev 相比输出有三处显著不同,均可在快照中直接验证:

  1. 死写入被拆成惰性 Part。dev 里foobarCopy += "Unused"console.log同在 Part 7;prod 里 Part 7 只剩console.log(foobarCopy)(并直接import part 0代替 dev 的 part 1 包装),而那条没有任何读者消费其结果的写入被单独移到惰性 Part 8:
// prod Part 7 import { d as foobarCopy } from "__TURBOPACK_PART__" assert { __turbopack_part__: -5 }; import "__TURBOPACK_PART__" assert { __turbopack_part__: 0 }; import "__TURBOPACK_PART__" assert { __turbopack_part__: 6 }; console.log(foobarCopy); export { }; // prod Part 8(惰性) import { d as foobarCopy } from "__TURBOPACK_PART__" assert { __turbopack_part__: -5 }; foobarCopy += "Unused";
  1. 可提升函数被进一步合并。dev 下internal独占 Part 8、external1独占 Part 9;prod 下两者连同import { upper }合并进同一个 Part 9(function internal() {...} function external1() {...}),因为它们都是 Hoisted 声明,定义何时加载不影响正确性,合并减少了 Part 数量。

  2. 导出直接贴在声明 Part 上。prod Part 3 直接export { foo }(入口映射Export("foo"): 3),不再需要 dev 那个专门 re-export 的 Part 11;foobar的导出则落在新 Part 11,该 Part 会先import part 6foobar += "foo")再export { foobar },保证消费者读到的是求值完成后的值。

从源码结构看,这三处差异正是handle_weak对不同Mode采用不同弱化策略的体现:dev 模式保守(保留更多前置依赖,保证断点与调试语义),prod 模式激进(把弱边拆成惰性 Part、合并提升声明)。

如何使用这份快照与测试机制

如果你要在本地验证或扩展这套分析测试:

  • 查看输入:1/input.js;查看期望快照:1/output.md;
  • 测试运行于turbopack-ecmascriptcrate 内,可通过cargo test(在 turbopack/Cargo.toml 定义的工作区根目录执行,按需以-p turbopack-ecmascript指定包)运行;fixture 机制会自动把每个input.js的输出与output.md比对,不一致即测试失败。注意快照是"期望值"文件,正常情况下不应手工修改;
  • 想测试"只消费某个导出"的场景,可参考带config.json的用例(如test-config-1):exports字段是Vec<Vec<String>>,每一组会额外输出一段以导出名命名的 "Merged (...)" 与入口映射(见 tests.rs 与 tests.rs)。

小结

output.md 用一份 18 行输入,完整记录了 Turbopack tree-shaker 的五个关键产物:17 个带提升/副作用/读写属性的分析项四阶段演进的强/弱依赖图连通压缩后的最终节点dev/prod 两套入口映射,以及基于__TURBOPACK_PART__/__TURBOPACK_VAR__断言的 13 个模块 Part 与合并结果。它同时回答了三个工程问题:语句级树摇如何在"立即语义"与"最终语义"之间做区分(Reads/Reads (eventual))、活绑定如何跨 Part 传递(__turbopack_var__导出)、以及 prod 模式如何利用弱依赖把死写入与提升声明拆成惰性/合并的 Part。对于需要理解 Next.js Turbopack 构建产物形态、或排查按需加载行为异常的开发者和 Agent 来说,这份快照与其驱动的 fixture 测试 是可复现、可验证的第一手材料。

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 17:09:21

如何用 JSON 配置快速搭建后台管理系统:amis 低代码框架实战指南

如何用 JSON 配置快速搭建后台管理系统&#xff1a;amis 低代码框架实战指南 【免费下载链接】amis 前端低代码框架&#xff0c;通过 JSON 配置就能生成各种页面。 项目地址: https://gitcode.com/GitHub_Trending/am/amis 做后台管理系统&#xff0c;你大概率写过这类重…

作者头像 李华
网站建设 2026/9/8 17:08:43

Hello Algo 图论章节核心总结:图的表示、遍历与常见疑问全解

Hello Algo 图论章节核心总结&#xff1a;图的表示、遍历与常见疑问全解 【免费下载链接】hello-algo 《Hello 算法》&#xff1a;动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語&#xff0c;提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ru…

作者头像 李华
网站建设 2026/9/8 17:07:54

基于ManTra-Net的图像篡改检测方法研究与工程落地实践

简介&#xff1a;面向图像篡改检测、数字取证研究与应用开发的学习者&#xff0c;这套资料围绕ManTra-Net深度卷积网络模型展开&#xff0c;旨在解决图像伪造检测与篡改区域定位问题&#xff0c;覆盖从模型训练到Web部署的完整链路。资源共25个文件&#xff0c;从训练好的h5模型…

作者头像 李华
网站建设 2026/9/8 17:06:45

智能体系统架构的核心:隔离、集成与治理实践

最近我把智能体&#xff08;AI Agent&#xff09;相关的系统架构翻来覆去捋了一遍&#xff0c;接触过一些从零搭起的智能体平台&#xff0c;也看过不少在生产环境里跑了一两年的老系统。圈里聊智能体&#xff0c;最热闹的永远是大模型又生成了什么&#xff0c;可真到企业落地时…

作者头像 李华
网站建设 2026/9/8 17:06:23

麒麟V10上安装Codebuddy:AI编程助手实战指南

开篇最近给自己手头的麒麟V10机器装了Codebuddy&#xff0c;折腾了大半天&#xff0c;从下载安装到真正跑起来一个“AI辅助写代码”的完整流程&#xff0c;中间踩的坑不算多但也不少。正好有朋友问我“麒麟V10上到底能不能用Codebuddy编程”&#xff0c;今天就系统地把整个过程…

作者头像 李华