Next.js Turbopack 模块切分机制解析:以 failed-1 树摇分析器快照为例

Next.js Turbopack 模块切分机制解析:以 failed-1 树摇分析器快照为例 Next.js Turbopack 模块切分机制解析:以 failed-1 树摇分析器快照为例【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js本篇基于 Next.js 仓库中 Turbopack 的测试快照文件turbopack/crates/turbopack-ecmascript/tests/tree-shaker/analyzer/failed-1/output.md,完整解读 Turbopack ECMAScript 模块切分器(tree-shaker analyzer)如何把一个普通 ES 模块拆分为多个可独立更新的 Part:从语句级 Items 分析、Phase 1~4 依赖图演化、Final 压缩图,到 dev/prod 两种模式下输出的__TURBOPACK_PART__/__TURBOPACK_VAR__代码形态。读完你能掌握 Turbopack 模块粒度切分(服务 HMR 与部分求值)的完整数据流,并学会如何阅读同类快照测试来验证切分行为。一、这份 output.md 是什么failed-1/output.md是 turbopack-ecmascript crate 中 fixture 测试的快照产物。测试驱动逻辑位于 module_fragments/tests.rs,其中通过#[fixture(tests/tree-shaker/analyzer/**/input.js)]声明:每个测试目录下的input.js都会被解析、分析,并生成一份与output.md逐字比对的结构化报告。output.md依次记录了:Items:输入模块被切分为哪些语句级条目,以及每个条目的变量声明/读写元数据;Phase 1 ~ Phase 4:依赖图在四个分析阶段的演化(mermaid 图);Final:压缩后的最终依赖图;Entrypoints:各入口到 Part 编号的映射;Modules (dev) / Modules (prod):开发模式与生产模式下实际切分出的代码片段。其输入文件 input.js 是一个典型的 Turbopack HMR 客户端模块,复现了浏览器端热更新 WebSocket 通道的结构:let source; const messageCallbacks []; function getSocketProtocol(assetPrefix) { let protocol location.protocol; try { protocol new URL(assetPrefix).protocol; } catch (_) {} return protocol http: ? ws : wss; } export function addMessageListener(cb) { messageCallbacks.push(cb); } export function sendMessage(data) { if (!source || source.readyState ! source.OPEN) return; return source.send(data); } export function connectHMR(options) { const { timeout 5 * 1000 } options; function init() { if (source) source.close(); console.log([HMR] connecting...); // handleOnline / handleMessage / handleDisconnect ... const { hostname, port } location; const protocol getSocketProtocol(options.assetPrefix || ); // ... 组装 url 并 new window.WebSocket(...) source new window.WebSocket(${url}${options.path}); source.onopen handleOnline; source.onerror handleDisconnect; source.onmessage handleMessage; } init(); }该模块的难点在于:两个模块级可变状态(source、messageCallbacks)被多个导出函数共享,且connectHMR内部通过闭包与setTimeout延迟读写source——这正是切分器必须正确建模变量跨 Part 共享与最终(eventual)依赖的场景。二、Items:语句级条目与变量元数据快照的# Items部分报告Count: 9:6 条顶层语句(Item 1~6)加上 3 个导出项(Item 7~9,对应export addMessageListener/export sendMessage/export connectHMR)。每个条目附带的元数据来自 tests.rs 中对Item结构的序列化(var_decls、read_vars、eventual_read_vars、write_vars、eventual_write_vars、is_hoisted、side_effects等字段)。逐条解读:条目语句关键元数据含义Item 1let source;Declares:source;Write:source声明模块级 WebSocket 句柄,初始为空Item 2const messageCallbacks [];Declares/Write:messageCallbacks声明回调列表Item 3function getSocketProtocol(...)Hoisted;Declares/Write:getSocketProtocol函数声明被提升,模块求值期即完成写入Item 4export function addMessageListener(cb)Hoisted;Reads (eventual):messageCallbacks;Write (eventual):messageCallbacks导出函数提升后声明即就绪;对messageCallbacks.push(cb)的读写发生在函数体内,属于 eventual(延迟到调用时)Item 5export function sendMessage(data)Hoisted;Reads (eventual)/Write (eventual):source同上,source.readyState、source.send都是 eventual 访问Item 6export function connectHMR(options)Hoisted;Reads (eventual):source、messageCallbacks、getSocketProtocol;Write (eventual):source、messageCallbacks同时依赖两个共享变量与私有函数getSocketProtocol这里有两个值得注意的建模细节:Hoisted(提升):函数声明在模块求值阶段即完成绑定,因此 Item 3~6 标记为 Hoisted,其Write发生在声明期而非调用期。Reads/Write (eventual):普通Reads/Write表示模块求值期间发生的访问;带(eventual)后缀的表示访问被推迟到函数体执行时(包括setTimeout(init, timeout)这类回调场景)。切分器据此决定变量必须通过 Part 间引用共享,而不能就地内联。三、Phase 1~4:依赖图的四个分析阶段tests.rs 中依次调用hoist_vars_and_bindings、evaluate_immediate、evaluate_eventual、handle_exports/handle_explicit_deps,每步之后用render_graph输出一张 mermaid 图,即快照中的# Phase 1~# Phase 4。四张图恰好展示了依赖边是如何逐层补齐的:Phase 1(变量与绑定提升后):图中 9 个节点(Item1~9)全部孤立,尚无任何--边。此时分析器只完成了语句收集与提升识别,还没建立谁依赖谁的关系。Phase 2(求值 immediate 依赖后):出现 3 条边——Item7 -- Item4; // export addMessageListener 依赖其函数语句 Item8 -- Item5; // export sendMessage 依赖其函数语句 Item9 -- Item6; // export connectHMR 依赖其函数语句即每个导出项挂到对应的函数声明语句上。Phase 3(求值 eventual 依赖后):再补齐 5 条边——Item4 -- Item2; // addMessageListener 事件级读写 messageCallbacks Item5 -- Item1; // sendMessage 事件级读 source Item6 -- Item1; // connectHMR 读写 source Item6 -- Item2; // connectHMR 读 messageCallbacks Item6 -- Item3; // connectHMR 调用 getSocketProtocol这 5 条正是变量声明语句 → 使用它的函数语句的依赖,全部来自 eventual 读写关系。Phase 4(处理显式依赖后)与 Phase 3 完全一致,说明该模块没有额外的显式依赖需要补充——依赖图在此收敛稳定。这一阶段划分让开发者能精确定位切分错误:若某条边缺失,快照 diff 会直接指出是哪个分析阶段遗漏了建模。四、Final:压缩图与 Entrypoints 映射# Final图把 9 个条目压缩为 5 个节点,每个节点是一组同属一个 Part 的条目:N0[Items: [ItemId(0, VarDeclarator(0))]]; // let source N1[Items: [ItemId(1, VarDeclarator(0))]]; // const messageCallbacks N2[Items: [ItemId(2, Normal), ItemId(5, Normal), ItemId(Export(connectHMR))]]; N3[Items: [ItemId(3, Normal), ItemId(Export(addMessageListener))]]; N4[Items: [ItemId(4, Normal), ItemId(Export(sendMessage))]]; N4 -- N0; N2 -- N1; N2 -- N0; N3 -- N1;节点语义与依赖边:N0 / N1:两个纯变量声明,各自成为一个独立的变量 Part,供其他 Part 以引用方式共享;N2:getSocketProtocol(Item 2)connectHMR(Item 5) 其导出项聚在一起,因为它既依赖source也依赖messageCallbacks(边N2 -- N0、N2 -- N1);N3:addMessageListener及其导出项,只依赖messageCallbacks(N3 -- N1);N4:sendMessage及其导出项,只依赖source(N4 -- N0)。紧随其后的# Entrypoints给出入口到 Part 编号的映射:{ ModuleEvaluation: 6, // 模块求值入口 → Part 6 Export(addMessageListener): 3, Export(connectHMR): 2, Export(sendMessage): 4, Exports: 5, // 纯导出聚合入口 → Part 5 }这些编号与下一节的## Part N一一对应:每个导出的更新入口指向独立 Part,使得sendMessage变更时只需重放 Part 4 的求值,而无需触碰其他 Part——这是 Turbopack 实现细粒度 HMR 与部分求值的基础。五、切分产物:__TURBOPACK_PART__与__TURBOPACK_VAR__快照的# Modules (dev)与# Modules (prod)两节分别对应 tests.rs 中handle_weak(Mode::Development)与handle_weak(Mode::Production)两次split_module的结果。本例两者输出完全一致,说明该输入下开发/生产模式的弱依赖处理没有差异。切分出的 7 个 Part 如下(完整代码见 output.md):Part 0 / Part 1 —— 变量 Part:// Part 0 let source; export { source as a } from __TURBOPACK_VAR__ assert { __turbopack_var__: true }; // Part 1 const messageCallbacks []; export { messageCallbacks as b } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };变量 Part 是切分器的核心机制:声明保留在独立 Part 中,并通过指向虚拟模块__TURBOPACK_VAR__的 re-export(带__turbopack_var__assert)把变量引用暴露出去,供消费方以绑定形式获取,保证各 Part 拿到的是同一个可变值而非拷贝。Part 2/3/4 —— 函数 Part:// Part 2(节选) import { b as messageCallbacks } from __TURBOPACK_PART__ assert { __turbopack_part__: -1 }; import { a as source } from __TURBOPACK_PART__ assert { __turbopack_part__: -0 }; function getSocketProtocol(assetPrefix) { /* ... */ } function connectHMR(options) { /* ... */ } export { connectHMR }; export { getSocketProtocol as c } from __TURBOPACK_VAR__ assert { __turbopack_var__: true }; export { connectHMR as d } from __TURBOPACK_VAR__ assert { __turbopack_var__: true };消费方通过指向__TURBOPACK_PART__的 import 拉取其他 Part 中的变量,assert { __turbopack_part__: N }指定目标 Part 索引(如-0、-1指向 Part 0/1);Part 自身声明的getSocketProtocol、connectHMR则同样经__TURBOPACK_VAR__导出,便于更高层的 Part 或模块继续引用。Part 3(addMessageListener)与 Part 4(sendMessage)结构相同,分别只 import 了 Part 1 与 Part 0 的变量。Part 5 —— 导出聚合 Part:export { connectHMR } from __TURBOPACK_PART__ assert { __turbopack_part__: export connectHMR }; export { addMessageListener } from __TURBOPACK_PART__ assert { __turbopack_part__: export addMessageListener }; export { sendMessage } from __TURBOPACK_PART__ assert { __turbopack_part__: export sendMessage };这是对应Exports: 5入口的 Part:不含任何实现,仅按导出名做跨 Part re-export,作为模块对外接口的稳定门面。Part 6 —— 模块求值 Part 与 Merged:// Part 6 export { }; // Merged (module eval) export { };本模块顶层没有产生副作用的立即求值语句(connectHMR内部的init()在函数体内,属于 eventual 行为),因此模块求值 Part 为空;Merged (module eval)表示合并后的模块求值段同样为空。六、如何验证与复现这份快照由 fixture 测试自动生成并比对:测试入口在 module_fragments/tests.rs(test_fixture收集tests/tree-shaker/analyzer/**/input.js,逐目录生成报告),切分与合并的实现位于 module_fragments/mod.rs 与 analyzer/graph/mod.rs 所在的turbopack-ecmascriptcrate 内。要在本地验证:查看turbopack/crates/turbopack-ecmascript/tests/tree-shaker/analyzer/下的兄弟用例(simple、grouping、shared-regression、effects-1等)可对照不同输入下的切分形态;修改 Turbopack 切分逻辑后,在 turbopack 工作区运行turbopack-ecmascriptcrate 的测试,若分析行为变化,对应目录的output.md快照会因 diff 失败而暴露问题——这正是failed-1这类回归用例存在的意义。适用前提与限制:该快照反映的是当前仓库版本 Turbopack 切分器的行为,Part 编号、__TURBOPACK_PART__/__TURBOPACK_VAR__等虚拟模块均为内部实现细节,可能随版本演进;阅读其他快照时应先打开对应input.js再对照 Items 章节,才能准确还原依赖关系。小结failed-1/output.md虽然是一份测试快照,但它完整保留了 Turbopack 模块切分的全链路证据:6 条语句 3 个导出项构成 9 个 Items;Phase 1~4 依次建立导出→语句→变量的三层依赖边;Final 图将条目压缩为 5 个节点;最终输出 7 个 Part,用__TURBOPACK_PART__import 与__TURBOPACK_VAR__export 把模块级可变状态(source、messageCallbacks)拆成独立变量 Part 并跨 Part 共享,每个导出获得独立更新入口。这套机制支撑了 Next.js 开发模式下 Turbopack HMR 的细粒度模块更新能力。【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考