ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Rolldown 代码分割下的执行顺序保持:`output.strictExecutionOrder` 与 `experimental.onDemandWrapping` 实战解析

Rolldown 代码分割下的执行顺序保持:`output.strictExecutionOrder` 与 `experimental.onDemandWrapping` 实战解析 Rolldown 代码分割下的执行顺序保持output.strictExecutionOrder与experimental.onDemandWrapping实战解析【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown本文围绕packages/rolldown/src/options/docs/output-code-splitting.md展开系统讲解 Rolldown 在代码分割code splitting场景下如何通过output.strictExecutionOrder与experimental.onDemandWrapping两个选项保持跨 chunk 的模块执行顺序。读完本文你将理解 manual code splitting 触发副作用顺序错乱的根本原因、wrap-all 与 on-demand 两种严格模式的取舍、init_*()触发机制的底层实现以及如何在配置与测试中实际启用和验证这些行为。一、问题背景manual code splitting 为什么会破坏执行顺序Rolldown 与 Rollup 兼容的 API 允许通过output.codeSplitting.groups等方式进行手动代码分割manual code splitting把若干模块显式归并到同一个 chunk。其声明定义位于 output-options.ts每个 group 拥有一个name同时作为 chunk 名并替换chunkFileNames中的[name]占位符和test匹配规则。原文档开头的:::warning明确指出一个关键风险如果副作用side effects在对应模块被真正使用之前就被触发手动代码分割可能改变应用的运行行为。这是因为模块一旦被物理地拆进不同 chunk加载顺序就不再严格等于源码中的 import 顺序。一个“提前被求值”的模块其顶层副作用会在依赖它的消费者运行之前执行从而造成与源码语义不一致的 observable 差异。Rolldown 为该问题提供两条解决路径二者都围绕“把模块体包装起来、延迟到正确时机再执行”这一核心思想output.strictExecutionOrderwrap-all全量包装模式把每个符合条件的模块都包装成延迟执行的函数experimental.onDemandWrapping按需包装模式基于预测的 chunk 执行风险推导出保守的、更小的包装计划。二、两个选项的官方定义速览2.1output.strictExecutionOrder在 output-options.ts 中定义如下保持生成的 chunk 之间源代码模块的执行顺序。启用后Rolldown 会包装 ESM 模块使它们无论 chunk 如何放置其函数体都按源码顺序执行。CommonJS 与 require-of-ESM 模块的 interop 包装保持不变外部模块external不受影响。experimental.onDemandWrapping会用从预测的 chunk 执行风险推导出的保守计划取代 wrap-all。// packages/rolldown/src/options/output-options.ts类型定义节选 /** * Preserve source module execution order across generated chunks. * ... * [!WARNING] * Enabling this option increases bundle size because wrapped modules need runtime init helpers. * default false */ strictExecutionOrder?: boolean;属性说明所属选项outputOutputOptions默认值false作用范围仅 ESM 模块CJS/require-of-ESM 的 interop 包装不变external 模块不受影响代价被包装的模块需要运行时init_*辅助函数bundle 体积增大2.2experimental.onDemandWrapping在 input-options.ts 中定义/** * Under output.strictExecutionOrder, derive a conservative wrapping plan from predicted * chunk execution hazards instead of wrapping every eligible module. * default false * hidden not ready for public usage yet */ onDemandWrapping?: boolean;属性说明所属选项experimentalInputOptions默认值false前置条件需在output.strictExecutionOrder: true的前提下生效状态hidden尚未对公众开放仅用于实验与内部验证三、wrap-allstrictExecutionOrder: true的默认行为3.1 工作原理从 internal-docs/code-splitting/design.md 的架构记录可以看到两种模式的定位strictExecutionOrder: truealone runswrap-all: every eligible module defers, the eager phase contains only inert definitions, and no evaluation-order prediction is needed — correctness rests solely on the shared lowering and trigger placement. It is the default because its trust base is the smaller one, and it serves as the escape hatch when the selective analysis misjudges a shape.wrap-all 的要点每个符合条件的模块都被包装eager 阶段只包含“惰性定义”inert definitions不需要做任何求值顺序预测正确性只依赖统一的 lowering 与 trigger 放置逻辑它是默认模式因为其“信任基础更小”当按需分析对某个图形结构判断失误时wrap-all 作为逃生舱口escape hatch兜底。3.2 信任基础与代价因为不依赖任何预测wrap-all 的正确性论证最简单所有可能被提前执行的模块一律延迟。代价是包装数量更多、体积更大——这正是 output-options.ts 中WARNING注释所提醒的 bundle-size 成本。3.3 为什么WrapKind不够用design.md 的第 7 行解释了架构层面的关键区别WrapKindCjs/Esm回答的是输入模块的表示问题参与 linking决定命名空间形状、绑定访问、require()行为与 tree-shaking 依赖而执行顺序保持回答的是输出布局问题——模块体是否必须被延迟因为生成的 chunk 图会过早执行它。因此顺序决策只能在临时 chunk 放置provisional chunk placement之后作出。若复用WrapKind::Esm来表达这个晚期决策就会让 generate-stage 的调度看起来像是新的 interop 事实还会让wrapper_ref、wrapper_stmt_info等 linking 阶段持有的字段暴露给晚期修改。最终架构选择详见下文第五节是linking 阶段的LinkingMetadata::wrap_kind()保持不可变顺序包装全部下沉到 generate-stage 独立的OrderWrapState侧表中。四、on-demandexperimental.onDemandWrapping的按需分析4.1 从“预测风险”出发的保守计划design.md 对 on-demand 模式的定义experimental.onDemandWrapping: trueopts into theon-demandanalysis described below, which starts from predicted evaluation-order hazards and conservatively closes the plan over cases where safety requires additional wrappers.流程是先从预测的求值顺序风险出发再在安全性要求额外包装的情况下保守地闭合计划。两种模式共享同一条 plan / lowering / consumer 流水线唯一的区别是计划如何被播种seeded。4.2 单向差异与 tree-shaking 一致性design.md 强调这种差异是单向的wrap-all may create more inert wrappers, but it must not retain or execute more user code. Link-stage statement and binding liveness is final before either plan is lowered, so wrap-all and on-demand preserve the same tree-shaking result.即wrap-all 可以多产生惰性包装但不能多保留或多执行用户代码。两种模式的 tree-shaking 结果完全一致因为它们共享同一个 linking 阶段语句与绑定活性在计划落地前已冻结。4.3 实现载体order_analysis.rs按需分析的核心实现在 crates/rolldown/src/stages/generate_stage/order_analysis.rs。从源码结构看其关键构件包括OrderAnalysis/OrderWrapPlanL44-L71包装计划的主体OrderWrapPlan内部就是一个FxHashSetModuleIdxwrap_all_order_analysisL697wrap-all 模式直接由 expected orders 播种跳过预测post_lowering_import_edgesL320emergent-cycle 不动点投影器——每轮把计划的init_*转发边叠加到predicted_static_import_edges基线图上找出被这些边闭合的 chunk SCC把环内每个符合条件且顺序敏感的模块标记为 at-risk重建计划直到 at-risk 集合不再增长is_order_wrap_eligibleL1194与is_order_sensitiveL1149判断模块是否“可被包装”与“顺序敏感”expected_order_for_root/actual_order_for_rootL715 / L784分别计算期望顺序与实际顺序用于对比找出差异。4.4 调试ROLLDOWN_ORDER_DEBUGorder_analysis.rs 提供ROLLDOWN_ORDER_DEBUG1环境变量在 stderr 输出 on-demand emergent-cycle 不动点的逐轮 SCC 数量与最终 wrap delta。该标志通过LazyLock只读取一次正常构建中禁用的 trace 只是一个原子加载零开销。官方注释提到它使“vue-vben-admin: 141 wraps in 2 iterations”这类原本难以验证的声明可以从构建产物中复现。五、触发机制与生成架构5.1init_*()触发放置无论哪种模式被包装的模块都通过统一的trigger触发点在正确时机执行。design.md 的“Trigger placement”一节给出了完整的放置清单design.md触发场景放置位置存活语句对 order-wrapped importee 的init_*()importer 函数体、语句位置被移除语句的init_*()/require_*()义务importer 函数体、被移除语句的位置生成式 CJS 再导出 interopconsumer-local barrelimporter 函数体、路由语句位置用户入口 / 动态入口的激活entry chunk prologue仅当其他 chunk 可能加载该实现 chunk 时才用 facade可折叠动态入口的激活importer 函数体、被改写的import()调用点facade 规则是其中的核心约束A trigger must never sit inline in a chunk body that other chunks can evaluate as a dependency.即触发点绝不能内联在“可能被其他 chunk 作为依赖求值”的 chunk 体中。如果一个 entry 的 chunk 可能被其他 chunk 加载静态 import 或跨 chunk 动态 import 其宿主模块则该 entry 的 trigger 必须移到 facade若只有它自己能加载自己则保留内联 trigger不额外产生文件。5.2OrderWrapState与FinalEsmInitMetadata为保证 linking 阶段数据不被晚期破坏design.md 提出两个关键数据结构OrderWrapStategenerate-stage 最终化阶段创建的侧表保存顺序包装专属状态wrapper 符号、每条 import record 的 CJS carrier、import overlay、合成语句等LinkingMetadata不镜像这些字段当不需要任何包装时该表保持为空。SealedFinalEsmInitMetadata由compute_wrapped_esm_init_metadata计算封装“某init_*()调用是否 no-op”与“每个被移除语句必须初始化哪些包装模块”两类最终事实。SealedT只有私有构造器、只允许Deref无法被 unwrap 或可变解引用从而在类型层面禁止跨 chunk linking 与模块最终化之外重新打开这些事实。对应的不变式design.md包括任何 generate-stage 调用都不能修改wrap_kind()任何顺序 lowering 都不能设置用户语句的 inclusion 位wrap-all 与 on-demand 必须保持相同的语句/绑定活性flag-off 构建不产生任何顺序包装或 strict-only facade。5.3 模块最终化的三种包装情形设计文档指出模块最终化器现在显式区分三种情形design.mdWrapKind::Cjs的 CJS interop 包装WrapKind::Esm的 ESM interop 包装OrderWrapState中的 execution-order 包装。第三种情形复用既有的提升式function init_*()代码形态wrapper 符号来自 order state最终派生事实来自FinalEsmInitMetadata永远不观察被覆盖的 interop kind。六、保守决策宁可多包不可错序design.md 的“Conservative decisions”一节列举了严格输出故意接受额外包装以保证确定性的主要位置design.mdwrap-all 模式包装一切符合条件的模块见第三节Chunk-cycle bailouton-demand某个 root 能沿预测边到达静态 chunk cycle 时额外包装其期望顺序中的每个符合条件模块——因为环内求值顺序取决于运行时先进入哪个 chunk而 lowering 本身会移动入口点预测无法看到另一环内 chunk 急切调用的var形式 interop wrapper 定义Entry-trigger facades任何其他 chunk 可能加载的 entry其 trigger 移到 facade判定依据是真实跨 chunk link 计算而非预测严格模式下跳过 CJS 命名空间合并determine_safely_merge_cjs_ns合并会把存活的require()调用移到保留语句处这种函数体内的移动无法靠包装修复因此选择跳过合并代价是每个 importer 调用点多占字节由 wrapper 备忘expected ∖ actual种子预测顺序中不可见、但 tree-shaking 视为无副作用、实际顺序敏感的模块一律包装而非信任。七、边界与不承诺TLA 与外部模块严格顺序包装对两类输入不做出排序承诺但保证输出合法可执行design.md顶层 awaitTLA顺序包装不会比默认构建提供更强的 TLA 语义。机制上保持合法——TLA 污染或传递依赖 TLA的模块得到asyncwrapper 函数体每个init_*()调用点在目标被污染时 awaitEsmInitTarget::tla_tainted污染随包装传播await永远不会落在同步函数中外部模块external对 external 的静态 ESM import 无法延迟它提升到 chunk 顶部、在 chunk 加载时求值因此当 importer 被包装时external 的副作用可能比源码顺序更早运行——对静态 ESM 输出这无法通过包装修复与其他打包器行为一致。但输出仍然合法external import 语句保留在 chunk 顶部被包装的 importer 在闭包内引用其绑定。八、实战配置与测试验证8.1 在 rolldown 配置中启用// rolldown.config.js import { defineConfig } from rolldown; export default defineConfig({ input: src/main.js, output: { dir: dist, codeSplitting: { groups: [ { name: libs, test: /node_modules/ }, ], }, strictExecutionOrder: true, // wrap-all所有符合条件的模块延迟执行 }, experimental: { // 可选用预测风险推导保守包装计划取代 wrap-all // 注意该选项当前 hidden尚未对公众开放 onDemandWrapping: false, }, });要点提醒strictExecutionOrder是output下的选项默认falseonDemandWrapping是experimental下的选项仅在output.strictExecutionOrder: true时生效开启后会显著增大 bundle 体积建议先在测量体积影响后再决定是否用于生产。8.2 仓库内的真实测试配置Rolldown 自带的 fixture 同时覆盖两种模式的对比见 on_demand_wrapping/_config.json{ configName: on-demand, config: { input: [ { name: main, import: ./main.js }, { name: dynamic, import: ./dynamic.js } ], strictExecutionOrder: true, experimental: { onDemandWrapping: true } }, expectExecuted: false, configVariants: [ { _configName: wrap-all, onDemandWrapping: false } ] }这个配置演示了 Rolldown 测试体系的标准做法同一组 fixture 通过configVariants在on-demand与wrap-all两个单元格间切换输出敏感的测试对两种模式各自生成 snapshot执行类测试则断言运行时行为与 tree-shaking 结果一致——这正是 design.md“验证”一节的要求。在crates/rolldown/tests/rolldown/function/experimental/strict_execution_order/目录下还有大量针对特定形态的定向用例例如cjs_reexport_barrel_*系列覆盖 CJS 再导出 barrel 的各种消费者路由与 fallbackemergent_cycle_*系列覆盖 emergent chunk cycle 投影top_level_await_syntax系列覆盖 TLA 污染传播dynamic_entry_retained_star_*系列覆盖动态入口的保留星导出。另有入口 facade 行为测试 strict-execution-order-entry-facade.test.ts。8.3 如何验证行为Rolldown 的验证策略design.md强调在可观察的编译器边界验证而非深入私有 pass 状态真实Bundler集成不变式覆盖 flag-off 传统输出、字节级一致的 on-demand 输出、wrap-all 行为、entry prologue 与 facade 放置、跨 chunk init 定义、runtime helper 闭包每个既有显式 strict 配置都有对应的 on-demand 配置如上面_config.json的configVariants定向 fixture 覆盖 retained barrel/namespace 路径、emergent chunk cycle、CJS init exports、user/dynamic/emitted entry facade、合法 TLA 输出、external 边界外部差分模糊测试器external differential fuzzer把普通源码执行与生成的 wrap-all / on-demand 输出在 ESM、CJS、混合模块、压缩、package 副作用元数据、命名空间读取、多种输出格式下逐一对比——这是最终的语义裁判完整的 Rust、Node、WASI、Vite、格式化、lint 与仓库校验作为合并门槛。九、总结与选型建议回到 output-code-splitting.md 的核心警告可以给出以下实操结论是否开启strictExecutionOrder如果你的应用通过codeSplitting.groups手动拆包且模块顶层存在副作用、拆包后观察到行为差异应开启它如果应用本身是纯 ESM 且顶层无副作用或可接受较小的体积收益则保持默认false。选 wrap-all 还是 on-demandwrap-all 是默认与兜底正确性论证简单、信任基础小on-demand 通过预测与不动点投影减少包装数量但代价是引入了预测逻辑与 emergent-cycle 修复循环order_analysis.rs且选项目前标注hidden未公开适合在实验分支中用ROLLDOWN_ORDER_DEBUG1观察其包装增量后自行评估。无论如何不要手动“修顺序”文档给出的替代方案是把顺序敏感模块放进同一 chunk 组或依赖strictExecutionOrder让包装器自动按源码顺序执行函数体。把副作用的正确性建立在手写加载顺序上在 chunk 图被优化器调整后会再次失效。进一步阅读internal-docs/code-splitting/design.md选择性严格执行顺序的完整架构设计目标、保守决策、数据流、不变式、被否决方案internal-docs/code-splitting/implementation.md当前实现细节与回归覆盖说明crates/rolldown/src/stages/generate_stage/order_analysis.rsstrictExecutionOrder分析与OrderWrapPlan的源码实现docs/in-depth/automatic-code-splitting.md自动代码分割的深入指南on_demand_wrapping/_config.json两种严格模式的对比测试配置。【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表