ARTICLE DETAIL

资讯详情

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

解读 Turbopack tree-shaker 分析快照:以 otel-core 用例拆解 Items、Phase 与模块划分

解读 Turbopack tree-shaker 分析快照:以 otel-core 用例拆解 Items、Phase 与模块划分 解读 Turbopack tree-shaker 分析快照以 otel-core 用例拆解 Items、Phase 与模块划分【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.jsTurbopack 是 Next.js 仓库中负责模块打包与编译的 Rust 实现。它内置了一套 ECMAScript 的摇树tree-shaking分析器而仓库 tests/tree-shaker/analyzer/otel-core/ 下的output.md正是该分析器针对一个真实 OpenTelemetry 模块otel-core生成的可读化快照它把模块拆成最小单元Items、展示依赖图如何分阶段收敛Phase、最终如何划分为多个可独立加载的代码片段Part并分别给出 dev 与 prod 两种模式下的输出。读完本文你将掌握如何阅读这类.md快照文件理解 Turbopack 树摇分析中ImportOfModule/ImportBinding/Normal三类 Item、Phase 图的边是如何生长的、Entrypoints 编号的含义以及__TURBOPACK_PART__/__TURBOPACK_VAR__伪导入的用途——这套能力同样适用于阅读该目录下其余几十个分析器用例。这份 output.md 是什么以及它是怎么产生的output.md不是手写的说明文档而是 Turbopack ECMAScript 分析器在跑完某个用例后按固定模板序列化出来的中间结果快照。它与同目录 input.js 一一对应输入是 input.js一个从 OpenTelemetry JS 生态环境变量读取模块中改编出的 ESM 文件输出是该模块经过 import 收集、依赖图构建、条目entrypoint绑定、模块划分之后的文本化转储。在turbopack/crates/turbopack-ecmascript/tests/tree-shaker/analyzer/下存放着几十个同类用例simple、dce、nanoid、amphtml-document、app-route、route-handler、node-fetch、next-response、nextjs-tracer等每个用例由input.js与output.md组成个别用例还带config.json如3/、test-config-1/来控制分析选项。因此这类文件本质上是测试基线与可视化产物用于回归校验同一输入必须得到同一分析结果。输入模块一个带默认值合并的环境读取器先看输入 input.js文件头带有 The OpenTelemetry Authors 的 Apache-2.0 版权声明import { DEFAULT_ENVIRONMENT, parseEnvironment } from ../../utils/environment; import { _globalThis } from ./globalThis; /** * Gets the environment variables */ export function getEnv() { var globalEnv parseEnvironment(_globalThis); return Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv); } export function getEnvWithoutDefaults() { return parseEnvironment(_globalThis); }它的语义非常典型从../../utils/environment导入两份内容——默认环境对象DEFAULT_ENVIRONMENT以及负责把宿主对象解析成环境变量的函数parseEnvironment从./globalThis导入当前宿主全局对象_globalThisgetEnv()先用parseEnvironment(_globalThis)解析出真实环境再通过Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv)把默认环境与真实环境合并真实值覆盖默认值getEnvWithoutDefaults()则不做默认值合并直接返回解析结果。对外仅暴露两个具名导出getEnv、getEnvWithoutDefaults。这样一个小且依赖清晰的模块恰好用来观察摇树分析器对 import 语句、函数声明与导出条目的切分方式。Items模块被切分成的 9 个最小单元快照第一部分是# Items开头给出Count: 9随后逐条列出其中 7 个具名单元。它的关键做法是一条 import 语句并不会作为一个整体参与分析而是会被拆成导入该模块含副作用与逐一导入每个绑定两类 Item。Item 1Stmt 0 的ImportOfModuleimport { DEFAULT_ENVIRONMENT, parseEnvironment } from ../../utils/environment;HoistedSide effects它是导入../../utils/environment这个模块本身。之所以标记Side effects是因为被导入模块在加载时可能执行顶层副作用代码而这段 import 不能仅因没人用它的导出就被无条件丢弃。Hoisted表示这类导入声明在语义上会被提升到模块最前执行。Item 2 与 Item 3Stmt 0 的两个ImportBinding同一行 import 中每个名字各占一个 Itemimport { DEFAULT_ENVIRONMENT, parseEnvironment } from ../../utils/environment;Item 2ImportBinding(0)—— HoistedDeclares:DEFAULT_ENVIRONMENTItem 3ImportBinding(1)—— HoistedDeclares:parseEnvironmentImportBinding(i)中的i是第几个绑定从 0 起。它只负责声明名字真正的模块副作用责任已经交给同语句的ImportOfModule因此绑定项不再重复标注 Side effects。Item 4 与 Item 5Stmt 1 的导入对第二行 import./globalThis同样拆成两个 Itemimport { _globalThis } from ./globalThis;Item 4ImportOfModule—— HoistedSide effectsItem 5ImportBinding(0)—— HoistedDeclares:_globalThisItem 6 与 Item 7两个Normal导出函数export function getEnv() { var globalEnv parseEnvironment(_globalThis); return Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv); }HoistedDeclares:getEnvReads (eventual):parseEnvironment,_globalThis,DEFAULT_ENVIRONMENTWrite:getEnvexport function getEnvWithoutDefaults() { return parseEnvironment(_globalThis); }HoistedDeclares:getEnvWithoutDefaultsReads (eventual):parseEnvironment,_globalThisWrite:getEnvWithoutDefaults这里最值得注意的属性是Reads (eventual)最终/延迟读取。函数声明体里的parseEnvironment、_globalThis、DEFAULT_ENVIRONMENT只有当函数真正被调用时才会读取属于可能发生的延迟依赖——分析器必须在后续 Phase 中把这些读取关系作为依赖边补充进图里而不是在函数定义处就当作立即求值。需要补充的是虽然Items明细只展开列了 7 项Count: 9与后面的 Phase 图都表明分析还产生了另外两个条目Item 8export getEnv与Item 9export getEnvWithoutDefaults。它们对应export出口本身在图里以Item8[export getEnv]、Item9[export getEnvWithoutDefaults]形式出现并在最终阶段与各自的函数体 Item 合并成同一个节点。从 Phase 1 到 Phase 4依赖图如何逐步生长并收敛第二部分展示分析器把 Item 之间的依赖关系分阶段叠加的过程每个 Phase 都是一张 mermaid 依赖图。图中同名的节点代表同一个 Item箭头A -- B表示A 依赖 B。Phase 1只有默认绑定依赖模块导入此时图中唯一的边是Item2 -- Item1声明DEFAULT_ENVIRONMENT的绑定Item 2依赖导入../../utils/environment模块Item 1。注意 Item 3parseEnvironment虽然来自同一行 import却尚未建立边——说明初始阶段并不是按语句整体建边而是按 Item 粒度逐步推算。Phase 2导出条目挂到函数体上新增两条边Item8 -- Item6导出getEnv依赖函数getEnv与Item9 -- Item7导出getEnvWithoutDefaults依赖对应函数。至此入口导出 → 被导出函数的骨架已经成形。Phase 3函数体的延迟读取被展开上一节标注的Reads (eventual)在这里全部落地为依赖边Item6 -- Item4、Item6 -- Item5getEnv读取./globalThis的模块导入Item 4与其绑定_globalThisItem 5Item6 -- Item3getEnv读取parseEnvironment绑定Item7 -- Item4、Item7 -- Item5getEnvWithoutDefaults同样读取_globalThis的模块导入与绑定。由此可以看到Object.assign({}, DEFAULT_ENVIRONMENT, ...)里的DEFAULT_ENVIRONMENTItem 2/Item 1只被getEnv依赖而getEnvWithoutDefaults与默认环境完全无关——这正是后续把getEnv与getEnvWithoutDefaults拆进不同 Part的依据之一。Phase 4图收敛不动点Phase 4 与 Phase 3 的边完全一致说明依赖关系已进入不动点没有新的依赖需要追加分析结束。这种逐轮把可传递依赖补进图中、直至不再变化的策略是分析器保证最终划分结果稳定的关键。Final按连通分量归并成节点收敛后的图再按可达/连通关系把 Item 归并成若干节点N0–N6每个节点将对应一个独立的代码片段从节点内容可以看到归并规则N0 / N1 / N2都源自第 0 行 import模块导入、DEFAULT_ENVIRONMENT绑定、parseEnvironment绑定被分到三个不同节点N3 / N4源自第 1 行 import./globalThis的模块导入与其绑定N5ItemId(2, Normal)getEnv函数体∪Export((getEnv, #2), getEnv)——导出项与函数体被合并在同一节点说明只要getEnv被需要就能连带整个函数体一起交付N6同理承载getEnvWithoutDefaults及其导出。节点间的边如N5 -- N1、N5 -- N4、N5 -- N2则表示生成该片段时仍需从其他节点拉取绑定/模块副作用。最终这张节点图就是后续 Part 划分的蓝图。Entrypoints谁被当作模块入口快照随后给出两份完全相同的# Entrypoints区块一份在模块片段展示前一份在Modules (dev)之后内容如下{ ModuleEvaluation: 3, Export( getEnv, ): 5, Export( getEnvWithoutDefaults, ): 6, Exports: 7, }它的含义是该模块有四个入口点各自落到对应的节点/Part 编号上ModuleEvaluation: 3——模块被求值执行副作用时的入口在 Part 3Export(getEnv): 5——具名导出getEnv的入口在 Part 5Export(getEnvWithoutDefaults): 6——具名导出getEnvWithoutDefaults的入口在 Part 6Exports: 7——汇总所有导出的聚合入口在 Part 7。也就是说消费者如果只 importgetEnv就只需拉取 Part 5 及其依赖如果只想让模块副作用被执行例如import module则只需 Part 3。这正是按入口切分、按需组合的划分结果。ModulesdevPart 0–7 的代码形态# Modules (dev)展示开发模式下划分出的 8 个 Part。它们是供读者理解划分结果的伪代码/中间形态其中模块内部跨 Part 的引用统一使用带__turbopack_part__属性import assertion的伪模块标识__TURBOPACK_PART__来表示。Part 0——环境模块的副作用导入import ../../utils/environment;它对应节点 N0单独保留加载../../utils/environment会产生副作用这件事供需要副作用的其他 Part 复用。Part 1 与 Part 2——绑定声明的占位片段import __TURBOPACK_PART__ assert { __turbopack_part__: 0 };Part 1 与 Part 2 内容相同都是再导入 Part 0。结合 Final 图中 N1、N2 都指向 N0 可知声明DEFAULT_ENVIRONMENT与parseEnvironment这两个绑定时需要先确保承载模块副作用的 Part 0 被执行。Part 3——模块求值片段Merged module evalimport __TURBOPACK_PART__ assert { __turbopack_part__: 0 }; import ./globalThis; export { };Part 3 是ModuleEvaluation入口对应的代码先拉取 Part 0执行环境模块副作用再真正import ./globalThis并以export { }保持模块语义。后面的Merged (module eval)区块与它完全一致表明合并后真正用于模块求值的片段就是 Part 3 本身。Part 4——导入求值片段import __TURBOPACK_PART__ assert { __turbopack_part__: 3 };Part 4 只是导入 Part 3对应节点关系 N4 -- N3./globalThis绑定需要其模块副作用。它是一个薄转发层确保引用方只要依赖 Part 4 就会连带触发 Part 3 的模块求值。Part 5——getEnv的完整实现import __TURBOPACK_PART__ assert { __turbopack_part__: 0 }; import { DEFAULT_ENVIRONMENT } from ../../utils/environment; import __TURBOPACK_PART__ assert { __turbopack_part__: 0 }; import { parseEnvironment } from ../../utils/environment; import __TURBOPACK_PART__ assert { __turbopack_part__: 3 }; import { _globalThis } from ./globalThis; function getEnv() { var globalEnv parseEnvironment(_globalThis); return Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv); } export { getEnv }; export { getEnv as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };对应节点 N5函数体getEnv 其导出项。可以观察到三个关键点该片段完整保留了对DEFAULT_ENVIRONMENT、parseEnvironment、_globalThis的真实 ESM 导入并在每个导入前后插入对应的__TURBOPACK_PART__转发导入保证绑定 → 模块副作用的顺序函数体代码与输入完全一致未做改写末尾除了export { getEnv }还多出一行export { getEnv as a } from __TURBOPACK_VAR__——其中的__turbopack_var__: true与别名a用于把导出注册到内部的导出变量机制上使下游可以按名字引用该导出这是一种内部簿记性质的导出而非给用户使用的名字。Part 6——getEnvWithoutDefaults的完整实现import __TURBOPACK_PART__ assert { __turbopack_part__: 3 }; import { _globalThis } from ./globalThis; import __TURBOPACK_PART__ assert { __turbopack_part__: 0 }; import { parseEnvironment } from ../../utils/environment; function getEnvWithoutDefaults() { return parseEnvironment(_globalThis); } export { getEnvWithoutDefaults }; export { getEnvWithoutDefaults as b } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };对应节点 N6。注意它只导入了parseEnvironment与_globalThis没有任何DEFAULT_ENVIRONMENT相关代码——与分析阶段 Read 信息、Final 节点依赖完全对应默认环境对象不会被打进只使用getEnvWithoutDefaults的产物里。Part 7——导出聚合器export { getEnv } from __TURBOPACK_PART__ assert { __turbopack_part__: export getEnv }; export { getEnvWithoutDefaults } from __TURBOPACK_PART__ assert { __turbopack_part__: export getEnvWithoutDefaults };Part 7 对应 Entrypoints 中的Exports: 7。它不包含任何实现只把两个具名导出从对应的 Part按export getEnv、export getEnvWithoutDefaults字符串寻址重新导出作为该模块的统一对外出口。Modulesprod开发与生产的分析结果在这里收敛# Modules (prod)在快照中紧接着出现其 Part 0–Part 7 以及Merged (module eval)与 dev 版本逐字完全相同。也就是说对于 otel-core 这个输入无论 dev 还是 prod分析器得出的模块划分、依赖顺序与代码片段都没有差别。出现这种情况是合理的——这份快照反映的是代码切分层面的分析结果真正的差异如变量名压缩、死代码删除的强度、热更新相关包装要等后续的代码生成/优化阶段才会体现。快照同时保留 dev/prod 两份输出正是为了在分析层单独校验两种模式在此阶段的一致性。在仓库中继续深挖的线索如果你希望从源码层面验证上面这些机制可以顺着以下路径继续阅读用例输入与基线输出otel-core/input.js、otel-core/output.md以及同目录的几十个兄弟用例如dce、nanoid、simple、node-fetch用于观察不同语法形态下的分析差异依赖图构建与延迟读取/副作用分析核心位于 turbopack-ecmascript/src/analyzer如 graph/mod.rs、imports.rs、side_effects.rs——它们负责把 import 拆分、收集绑定并把副作用归类ESM 引用与模块条目module item处理可参考 references/esm/module_item.rs最终把 Items 划分为 Part、输出__TURBOPACK_PART__形式片段的模块碎片机制位于 module_fragments 目录graph.rs 中即可找到对ImportOfModule等 ItemId 的引用可结合 graph.rs 与 merge.rs 理解 dev/prod 两条划分路径的合并与比较逻辑。小结如何快速读懂一张 tree-shaker 快照把 otel-core 这个用例读完整后可以总结出阅读同类output.md的四步法看 Items数出Count识别每条 import 被拆出的ImportOfModule副作用载体与逐个ImportBinding以及Normal语句携带的Declares / Reads (eventual) / Write属性——它们预告了后续所有依赖边的来源看 Phase逐张对比 mermaid 图的边直到某两个相邻 Phase 完全一致不动点即依赖收集结束再看 Final 图把 Item 归并成哪些节点、导出项与函数体是否同节点看 Entrypoints把ModuleEvaluation / Export(...) / Exports的编号与 Final/Part 编号对上理解求值入口、单导出入口、聚合入口各在哪个片段看 Modulesdev / prod核对每个 Part 保留了哪些真实 import、通过__TURBOPACK_PART__转发了哪些 Part、末尾用__TURBOPACK_VAR__注册了哪些别名导出再对比 dev 与 prod 是否一致。掌握这四步tests/tree-shaker/analyzer/下任何一张.md快照都能反过来告诉你对于给定的真实模块Turbopack 会怎样裁剪它的依赖、把哪些代码捆在一起、又把哪些代码彻底留在产物之外。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表