ARTICLE DETAIL

资讯详情

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

Rolldown 手动代码分割(Manual Code Splitting)完全指南:配置、缓存优化与边界行为

Rolldown 手动代码分割(Manual Code Splitting)完全指南:配置、缓存优化与边界行为 Rolldown 手动代码分割Manual Code Splitting完全指南配置、缓存优化与边界行为【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown本文围绕 Rolldown采用 Rollup 兼容 API 的 Rust 版 JavaScript/TypeScript 打包器的output.codeSplitting.groups手动代码分割能力展开讲解它如何与自动代码分割协同工作、如何通过分组配置减少部署时的缓存失效、如何利用浏览器并行加载提升首屏性能并深入剖析runtime.js强制生成、分组越界捕获依赖、maxSize非严格上限等边界行为的底层原理。读完本文你将能够为实际项目设计出既符合缓存策略又兼顾加载性能的手动分组方案并能预判配置在边缘场景下的表现。前置阅读与自动代码分割的关系在深入手动代码分割之前需要先理解 Rolldown 的自动代码分割机制。自动代码分割是不可控的它只按照固定规则把静态相连的模块静态import与require组合成 chunk由用户input配置产生的initial chunks以及由动态import()产生的dynamic chunks。它不考虑加载性能也不考虑缓存失效问题。需要首先澄清一个重要概念自动代码分割与手动代码分割并不矛盾使用手动代码分割并不意味着关闭自动代码分割。一个模块要么被自动代码分割捕获要么被手动代码分割捕获两者不会同时生效如果一个模块没有被任何手动分组捕获它仍会被放入自动代码分割创建的 chunk 中并遵循自动代码分割指南中说明的规则。从源码结构上也能印证这一点CodeSplittingMode枚举见 code_splitting_mode.rs区分三种形态——Bool(true)默认自动代码分割 动态导入懒加载、Bool(false)将所有动态导入内联进单一 bundle、Advanced(...)对象形式自动代码分割仍然开启只是叠加了用户指定的分组配置。也就是说codeSplitting传入对象时自动分割的通道并未关闭手动分组只是在其上截流了一部分模块。为什么需要手动代码分割自动代码分割完全不考虑加载性能与缓存失效它仅仅根据静态导入关系对模块分组这可能导致两个问题次优的 chunk 划分可能产生体积过大的 chunk不利于加载每次部署都引发全量缓存失效只要应用代码有任意一处改动整个 bundle 的 hash 都会变化浏览器不得不重新下载整个文件。手动代码分割正是为弥补这些短板而设计的。如何使用手动代码分割先看一个最典型的场景。假设有这样一个 React 应用入口// index.jsx import * as ReactDom from react-dom; import App from ./App.jsx; ReactDom.createRoot(document.getElementById(root)).render(App /); // App.jsx import * as React from react; import { Button } from ui-lib; export default function App() { return Button onClick{() alert(Button clicked!)} /; }不经任何手动干预时Rolldown 会输出一个合并后的文件示例中的 hash 编号示意内容变化// node_modules/react/index.js React library code; // node_modules/ui-lib/index.js UI library code; // node_modules/react-dom/index.js ReactDOM library code; // App.js function App() { return Button onClick{() alert(Button clicked!)} /; } // index.js ReactDom.createRoot(document.getElementById(root)).render(App /);该示例使用了 3 个第三方库react、react-dom、ui-lib。output-hash0.js是 Rolldown 生成的输出文件hash0是输出文件的哈希只要文件内容变化哈希就会变化。实战场景一减少缓存失效这里的缓存失效指的是当部署新版本应用时浏览器需要下载新版本文件如果文件很大会带来糟糕的用户体验。例如仅修改app.jsx中的一个文本function App() { return Button onClick{() alert(Button clicked!)} /; // [!code --] return Button onClick{() alert(Button clicked!!!)} /; // [!code ] }那么自然得到一个新的output-hash1.js——除App函数那处改动外其余内容与output-hash0.js完全相同。但由于文件哈希变了浏览器会把它当作全新文件整体重新下载即使只有很小一部分发生变更。解决方案就是使用codeSplitting选项把不常变化的库代码拆到独立 chunkexport default { // ... other configurations output: { codeSplitting: { groups: [ { test: /node_modules/, name: libs, }, ], }, }, };使用上述配置后输出变为两个文件import ... from ./libs-hash0.js; // App.js function App() { return Button onClick{() alert(Button clicked!)} /; } // index.js ReactDom.createRoot(document.getElementById(root)).render(App /);// node_modules/react/index.js React library code; // node_modules/ui-lib/index.js UI library code; // node_modules/react-dom/index.js ReactDOM library code; export { ... };再次修改app.jsx后import ... from ./libs-hash0.js; // App.js function App() { return Button onClick{() alert(Button clicked!!!)} /; } // index.js ReactDom.createRoot(document.getElementById(root)).render(App /);// node_modules/react/index.js React library code; // node_modules/ui-lib/index.js UI library code; // node_modules/react-dom/index.js ReactDOM library code; export { ... };效果一目了然libs-hash0.js内容未变浏览器可直接使用缓存版本output-hash1.js内容变化浏览器只需下载这一份小文件。实战场景二提升加载性能手动代码分割还能通过把应用拆成合理数量的 chunk利用浏览器并行加载能力提升加载性能。上一个例子把所有库放进单一 chunk如果库体积过大浏览器下载时间会很长。解决方法是按库拆分成多个 chunk让浏览器并行下载export default { // ... other configurations output: { codeSplitting: { groups: [ { test: /node_modules\/react/, name: react, }, { test: /node_modules\/react-dom/, name: react-dom, }, { test: /node_modules\/ui-lib/, name: ui-lib, }, ], }, }, };输出变成import ... from ./react-hash0.js; import ... from ./react-dom-hash0.js; import ... from ./ui-lib-hash0.js; // App.js function App() { return Button onClick{() alert(Button clicked!)} /; } // index.js ReactDom.createRoot(document.getElementById(root)).render(App /);React library code; export { ... };ReactDOM library code; export { ... };UI library code; export { ... };现在库被拆分为独立 chunk浏览器可并行下载尤其在库体积较大的场景下能显著改善加载性能。配置项全景codeSplitting.groups 的完整参数codeSplitting的对象形式在 Rust 侧由ManualCodeSplittingOptions定义见 manual_code_splitting_options.rs其顶层字段与分组级字段如下顶层codeSplitting对象字段字段类型说明groupsMatchGroup[]手动分组列表本文核心includeDependenciesRecursivelyboolean分组是否递归捕获依赖默认trueminSizenumber分组最小字节数低于此值不生成 chunkmaxSizenumber分组目标最大字节数非严格上限见下文minShareCountnumber模块被至少 N 个入口共享才纳入分组minModuleSize/maxModuleSizenumber单个模块的大小过滤阈值groups中每个MatchGroup的字段字段类型说明namestring \| function生成的 chunk 名函数形式按模块批量返回名字可返回null跳过teststring \| RegExp \| function模块匹配规则函数形式批量接收ids: string[]返回每位0/非0字节标记是否捕获prioritynumber分组优先级值越大越优先默认0minSize/maxSizenumber覆盖顶层的分组级大小约束minShareCountnumber覆盖顶层的共享计数阈值minModuleSize/maxModuleSizenumber覆盖顶层的模块级过滤entriesAwareboolean是否按入口感知拆分默认falseentriesAwareMergeThresholdnumber仅当entriesAware: true时生效低于该大小的子分组会被合并tagsstring[]按模块标签过滤只有包含全部指定标签的模块才被捕获includeDependenciesRecursivelyboolean覆盖顶层的递归捕获开关从绑定层源码binding_manual_code_splitting_options.rs可以看到test接受string | RegExp | ((ids: Arraystring) Uint8Array)name接受string | ((ids, ctx) Arraystring | null)函数形式中还能通过ctx.getModuleInfo(id)查询模块元信息。Rust 内部把这两种函数统一为MatchGroupTest::Regex/Function与MatchGroupName::Static/Dynamic处理。仓库自带的配置示例output-code-splitting-example.md展示了多分组优先级与基于大小的拆分两种典型用法export default defineConfig({ output: { codeSplitting: { groups: [ { name: react-vendor, test: /node_modules[\\/]react/, priority: 20, }, { name: ui-vendor, test: /node_modules[\\/]antd/, priority: 15, }, { name: vendor, test: /node_modules/, priority: 10, }, { name: common, minShareCount: 2, minSize: 10000, priority: 5, }, ], }, }, });export default defineConfig({ output: { codeSplitting: { groups: [ { name: large-libs, test: /node_modules/, minSize: 100000, // 100KB maxSize: 250000, // 250KB priority: 10, }, ], }, }, });test未匹配到任何模块的 group 会被直接忽略源码中captured.is_empty()时continue因此在不确定路径规则时先写一个兜底的/node_modules/分组作为保守策略是常见做法。源码视角手动分组的执行流程Rolldown 在generate阶段通过GenerateStage::apply_manual_code_splitting驱动整个流程见 manual_code_splitting.rs其核心ManualSplitter::split分四步构建模块分组build_module_groups遍历所有已包含is_included且未被分配的普通模块对每个 group 先跑test匹配再跑passes_static_filters校验tags、minModuleSize/maxModuleSize、minShareCount最后调用name得到分组名并递归加入模块及其依赖处理 entriesAware 分组process_entries_aware_groups按每个模块的入口位图bits模式拆分子组再按entriesAwareMergeThreshold合并过小的子组按优先级排序into_priority_sorted_groups优先级高的 group 优先同优先级按 group 定义顺序再按名称字典序转换为 chunkconvert_groups_to_chunks检查minSize不足则丢弃该组超过maxSize时尝试基于模块路径相关性拆分否则直接发射为 chunk。两个值得注意的实现细节test与name是批量调用的因为它们是 JS 回调若逐个模块调用会跨越 napi 边界 M×G 次。源码注释明确说明改为每个 group 一次批量调用MatchGroupTestFn的返回值必须与传入的 module id 数组等长否则报错。确定性保证候选模块在调用用户提供的name函数前会按stable_id排序避免因并行resolveId/load导致的ModuleIdx顺序不稳定从而保证有状态函数的调用顺序跨运行一致。分组之间出现模块竞争时由priority决定最终归属convert_groups_to_chunks在发射某个 group 的 chunk 时会从其余 group 中移除已被占用的模块remaining.remove_module这正是高优先级分组优先抢占模块的实现方式。限制与边界行为为什么总是会生成一个runtime.jschunktl;dr只要使用groups做了手动代码分割Rolldown 就会强制生成一个runtime.jschunk以确保运行时代码始终先于其他任何 chunk 执行。runtime.js是一个特殊 chunk只包含加载和执行应用所必需的运行时代码。由于手动代码分割允许把模块在 chunk 之间搬移输出代码中很容易产生循环导入。循环导入可能导致运行时代码未先于其他 chunk 执行而引发错误。看一个产生循环导入的输出示例// first.js import { __esm, __export, init_second, value$1 as value } from ./second.js; var first_exports {}; __export(first_exports, { value: () value$1 }); var value$1; var init_first __esm({ first.js() { init_second(); // ... }, }); export { first_exports, init_first, value$1 as value }; // main.js import { first_exports, init_first } from ./first.js; import { __esm, init_second, second_exports } from ./second.js; var init_main __esm({ main.js() { init_first(); init_second(); // ... }, }); init_main(); // second.js import { init_first, value } from ./first.js; var __esm ...; var __export ...; var second_exports {}; __export(second_exports, { value: () value$1 }); var value$1; var init_second __esm({ second.js() { init_first(); // ... }, }); export { __esm, __export, init_second, second_exports, value$1 };当运行node ./main.js时模块的遍历顺序是main.js→first.js→second.js而实际执行顺序是second.js→first.js→main.js。问题在于second.js在执行时试图调用尚未初始化的__esm函数导致调用undefined的运行时错误。强制生成的runtime.js保证任何依赖运行时代码的 chunk 都会先加载runtime.js再执行自身从而杜绝上述循环导入问题。从源码实现看code_splitting.rsextract_standalone_runtime_chunk会把运行时模块单独抽出且 chunk 名称固定为rolldown-runtime。源码注释特别强调Vite、vitejs/plugin-rsc等下游工具正是靠这个名字识别运行时 chunk若改掉会静默破坏它们。为什么分组里会包含不满足约束的模块当一个模块被某个 group 捕获时Rolldown 默认会递归地捕获它的依赖且不考虑这些依赖是否满足约束。原因是默认情况下 Rolldown 只允许改写非入口 chunk 的导出。举例说明。假设代码如下// entry.js import { value } from ./a.js; console.log(value); export const foo foo; // a.js import { value as valueB } from ./b.js; export const value a valueB; // b.js export const value b;如果只想把a.js移入独立 chunk同时让b.js留在entry.js所在的 chunk实际输出会是import { value } from ./a.js; // b.js const value b; // entry.js const foo foo; console.log(value); export { foo, value };import { value } from ./entry.js; // a.js export const value a value;可以看到为了让a.js正常工作Rolldown 被迫改变了入口 chunkentry.js的导出签名额外导出了value。这完全违背了代码原本的意图——entry.js只想导出foo。如果不希望出现这种行为可以设置codeSplitting.includeDependenciesRecursively: false来关闭递归捕获。:::warning 注意事项当includeDependenciesRecursively: false时group 所依赖的模块可能残留在入口 chunk 中。但从入口 chunk 导出非入口模块是非法的为避免这种非法输出Rolldown 会在你未显式设置时隐式将preserveEntrySignatures设为allow-extension。includeDependenciesRecursively: false会提高生成非法输出代码的概率。如果因执行顺序或循环依赖遇到问题可以考虑开启strictExecutionOrder: true。:::这两条约束在仓库中有直接的实现与测试印证chunk_optimizer.rs 中存在对PreserveEntrySignatures::AllowExtension的分支处理逻辑测试 include_dependencies_recursively_conflict_preserve_entry_signatures/_config.json 表明若显式配置preserveEntrySignatures: strict同时又设置includeDependenciesRecursively: falseRolldown 会直接报配置错误expectError: true因为二者语义冲突。为什么 chunk 会比maxSize更大maxSize更像一个目标值而非严格上限。在以下场景中chunk 可能超过该值单个模块本身大于maxSize结果 chunk 必然超限。Rolldown 目前不支持把单个模块拆成多个 chunkminSize优先级更高如果拆分大 chunk 会产生低于minSize阈值的新 chunkRolldown 会保留原 chunk 不拆分避免生成过多过小的碎片文件。源码中convert_groups_to_chunks的实现与之一致只有group.sizes allow_max_size且能通过try_split_oversized_group找到合法切分点时才会拆分。而find_relevance_split_index在寻找切分点时会先计算左右两侧分别满足minSize的边界再在候选切分点中综合比较模块路径相似度优先在目录边界处切开与两侧最大尺寸。manual_code_splitting.rs内嵌的单元测试如min_size_prevents_split、similarity_prefers_low_stable_id_boundary直接验证了切分后任一侧低于minSize则放弃拆分的行为。小结Rolldown 的手动代码分割通过output.codeSplitting.groups提供了一套介于完全自动与完全手动之间的精细控制手段testname负责筛选与命名priority处理分组竞争minSize/maxSize/minShareCount/entriesAware/tags等参数提供多维度的约束能力而includeDependenciesRecursively则控制是否连带捕获依赖。使用时应牢记三条边界手动分割不会关闭自动分割两者互补而非互斥使用groups必然伴随强制生成的rolldown-runtimechunk这是为了规避循环导入导致的运行时错误maxSize是目标而非硬上限minSize优先于它且单模块无法再拆分。理解了这些原理你就能在真实项目中把减少缓存失效与提升加载性能这两个目标落地为具体可维护的codeSplitting配置并在遇到分组异常时快速定位原因。如需了解自动代码分割的完整行为规则可继续阅读 自动代码分割分组配置的完整字段定义可查阅 manual_code_splitting_options.rs。【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表