ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

深入 Next.js 中 Turbopack Tree Shaker 分析器:从一份测试快照看懂依赖图构建与模块切分全流程 深入 Next.js 中 Turbopack Tree Shaker 分析器从一份测试快照看懂依赖图构建与模块切分全流程【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本文以 Turbopack 的 ECMAScript 模块处理 crateturbopack-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-effects、next-response、template-pages等针对真实场景的用例。每个用例的input.js被 swc 解析为模块 AST并先运行resolverpass 做变量解析随后交给分析器tests.rs 还支持读取同目录下的config.json指定仅拉取哪些导出来测试部分用例如test-config-1附带该配置用例 1 无配置走默认全量分析。最终把每一步的中间产物序列化为 Markdown用NormalizedOutput::compare_to_file与output.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 中按固定顺序执行g.init(...)遍历 AST抽取全部分析项Itemsanalyzer.hoist_vars_and_bindings()处理变量/函数声明提升analyzer.evaluate_immediate(...)求值顶层立即执行的语句建立语句间依赖analyzer.evaluate_eventual(...)处理提升函数体中的最终调用时才会发生读写analyzer.handle_exports(...)为每个导出建立分组节点analyzer.handle_explicit_deps()处理显式依赖g.finalize(...)把强连通的项压缩成最终节点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)从该模块导入的具体绑定upperVarDeclarator(0)顶层变量声明语句let/constNormal普通语句或函数声明。每个项附带一组属性字段定义可见 tests.rs 的打印逻辑结构体位于 module_fragments/mod.rs属性含义Hoisted声明可提升定义可被延迟到真正使用时加载Side effects求值有副作用不可随意删除Declares声明的顶层绑定Reads/Reads (eventual)立即读 / 最终读函数被调用时才发生Write/Write (eventual)立即写 / 最终写17 个项逐一对照输入代码#语句种类关键属性1Stmt 0import { upper } from module;ImportOfModuleHoistedSide effects导入模块本身有求值副作用2Stmt 0import { upper } from module;ImportBinding(0)HoistedDeclares:upper3Stmt 1export let foobar foo;VarDeclarator(0)Declares:foobarWrite:foobar4Stmt 2export const foo foobar;VarDeclarator(0)Declares:fooReads:foobarWrite:foo5Stmt 3const bar bar;VarDeclarator(0)Declares:barWrite:bar6Stmt 4foobar bar;NormalReads:bar,foobarWrite:foobar7Stmt 5let foobarCopy foobar;VarDeclarator(0)Declares:foobarCopyReads:foobarWrite:foobarCopy8Stmt 6foobar foo;NormalReads:foobarWrite:foobar9Stmt 7console.log(foobarCopy);NormalSide effectsReads:foobarCopy10Stmt 8foobarCopy Unused;NormalReads:foobarCopyWrite:foobarCopy11Stmt 9function internal() { ... }NormalHoistedDeclares:internalReads (eventual):upper,foobarWrite:internal12Stmt 10export function external1() { ... }NormalHoistedDeclares:external1Reads (eventual):internal,foobarWrite:external113Stmt 11export function external2() { ... }NormalHoistedDeclares:external2Write:external2Write (eventual):foobar14导出分组Group标注为export foobar15导出分组Group标注为export foo16导出分组Group标注为export external117导出分组Group标注为export external2三个值得注意的细节一条导入语句被拆成两个项。Item 1ImportOfModule代表模块求值的副作用Item 2ImportBinding(0)代表对upper这个具体绑定的使用——这正是后续 dev/prod 能做出不同加载策略的基础。eventual前缀区分现在发生与将来发生。internal函数声明本身立即执行只是注册一个函数对象但它对upper、foobar的读取要等函数被调用时才发生所以标为Reads (eventual)同理external2对foobar的写入是Write (eventual)。foo与foobar是别名关系。const foo foobar之后两者指向同一绑定这一点会体现在后文 Item 6 到 Item 4 的弱边Item6 -.- Item4中。Phase 1–4依赖图的四次演进快照的 Phase 1 到 Phase 4 各给出一张 Mermaid 有向图。边分两种实线A -- B是强依赖Dependency::StrongB 必须先于 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 -- Item5、Item6 -- Item3foobar bar依赖bar与foobar的声明Item6 -.- Item4因为foo是foobar的别名对foobar的写与foo之间只是弱关联Item7 -- Item6、Item7 -- Item3let foobarCopy foobar的读取先于本次的结果Item8 -- Item6、Item8 -- Item3、Item8 -.- Item7foobar foo的执行顺序依赖Item9 -- Item7、Item9 -- Item1、Item9 -- Item8console.log(foobarCopy)有副作用必须执行且要求模块导入Item 1已完成、foobarCopy已被赋值其中Item9 -.- Item2对upper绑定的依赖与Item9 -.- Item11可能间接涉及internal是弱边Item10 -- Item7、Item10 -.- Item9foobarCopy Unused依赖拷贝值但对console.log的依赖是弱的导出分组边Item14 -- Item8、Item14 -- Item3导出foobar依赖其全部立即写、Item15 -- Item4导出foo、Item16 -- Item12导出external1、Item17 -- Item13导出external2。Phase 3最终求值之后提升函数补上调用时的边这是图中变化最大的一步全部来自三个提升函数的闭包分析Item11 -- Item2internal调用upper需要导入绑定Item11 -- Item8、Item11 -- Item3internal最终读取的foobar必须包含foobar fooItem 8之后的值且依赖声明Item 3Item12 -- Item11、Item12 -- Item8、Item12 -- Item3external1依赖internal及foobar的最终状态Item13 -.- Item14external2对foobar的最终写与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 bar与foobar bar合并执行紧邻、强连通N7[Item(7), Item(8)]console.log与foobarCopy Unused同节点N9[Item(10), Export(external1)]函数声明与其导出分组合并N10[Item(11), Export(external2), Export(foobar)]external2声明与两个导出分组同节点——因为external2持有对foobar的最终写导出foobar必须与它共存N11[Export(foo)]。最终图中的弱边N7 -.- N8、N7 -.- N1、N6 -.- N5、N4 -.- N3正是后面 prod 模式可以偷懒的地方。Entrypoints入口键到 Part 的映射切分完成后split_module返回SplitModuleResult { modules, entrypoints, .. }结构定义在 graph.rsentrypoints是一张入口键 → 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)键指向持有该导出的 PartExports: 12指向 Part 12 的导出门面统一 re-export 所有导出两模式的差异在于foo与foobardev 下foo的导出被拆到独立的 Part 11、foobar的导出与external2合并在 Part 10prod 下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 2foobar 的声明活绑定 a let foobar foo; export { foobar as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true }; // Part 3foo 的声明通过 -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 4bar 声明 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 11dev 下 Export(foo) 入口从 Part 3 的活绑定 re-export import { b as foo } from __TURBOPACK_PART__ assert { __turbopack_part__: -3 }; export { foo }; // Part 12Exports 门面按入口名字符串统一转发 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)测试用 Mergermerge_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 相比输出有三处显著不同均可在快照中直接验证死写入被拆成惰性 Part。dev 里foobarCopy Unused与console.log同在 Part 7prod 里 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;可提升函数被进一步合并。dev 下internal独占 Part 8、external1独占 Part 9prod 下两者连同import { upper }合并进同一个 Part 9function internal() {...} function external1() {...}因为它们都是 Hoisted 声明定义何时加载不影响正确性合并减少了 Part 数量。导出直接贴在声明 Part 上。prod Part 3 直接export { foo }入口映射Export(foo): 3不再需要 dev 那个专门 re-export 的 Part 11foobar的导出则落在新 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-1exports字段是VecVecString每一组会额外输出一段以导出名命名的 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),仅供参考
返回列表