Rolldown Lazy Compilation 设计解析:基于 /@vite/lazy 的动态导入按需编译机制
2026/9/15 22:53:58 网站建设 项目流程

Rolldown Lazy Compilation 设计解析:基于 /@vite/lazy 的动态导入按需编译机制

【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown

Rolldown 的 Lazy Compilation(懒编译)是一种开发期优化:动态导入(import())背后的模块不再随入口打包,而是等浏览器在运行时真正请求时才即时编译并返回。本文以 design.md 为主体,结合 implementation.md 与crates/rolldown_plugin_lazy_compilation/下的源码,系统讲解它的启用方式、作用范围、编译粒度、代理模块双状态模型、rolldown:exports契约以及 Dev Server 集成细节。读完你既能快速在 dev 模式下开启该特性,也能理解/@vite/lazy请求背后的完整调用链与安全设计。

什么是 Lazy Compilation

Lazy compilation 是一种开发优化手段,把动态导入模块的编译工作推迟到运行时真正被请求时才执行。其目标有三:

  1. 更快的冷启动—— 启动时只编译入口及其同步依赖;
  2. 按需编译——import()背后的代码在浏览器执行到该语句时才即时编译(just-in-time);
  3. 对用户透明—— 不需要修改业务代码,import('./foo')依旧"开箱即用"。

它复用了 HMR 的运行时与渲染路径来产出模块输出,因此与 HMR 模块系统天然配套,属于 dev/HMR 体系内的一项独立特性。

启用方式:opt-in 的 devMode 配置

Lazy compilation 是**主动选择(opt-in)**的特性,且必须嵌套在 dev 模式内启用:

export default { experimental: { devMode: { lazy: true }, }, };
  • 单独的devMode只会开启 dev/HMR 机制(即HmrPlugin);lazy: true才会额外把LazyCompilationPlugin插入到用户插件之前
  • 内部插件的注册逻辑位于 apply_inner_plugins.rs:当options.experimental.dev_mode存在时先压入HmrPlugin,当dev_mode.lazy == Some(true)时再创建LazyCompilationPlugin并压入(第 41–48 行)。这些内置插件总是被前置到用户插件之前,再通过PluginHookMeta机制控制钩子最终执行顺序,用户几乎感知不到它们的存在。
  • 插件通过context()方法暴露共享的lazy_entries/fetched_entries集合,封装为LazyCompilationContext,并交给DevEngine,使引擎能在每次懒编译前调用mark_as_fetched
  • rolldown-vite 的 bundled dev 模式默认开启lazy: true

仓库里的真实示例可见 examples/lazy-compilation/dev.config.mjs,它同时打开了devMode.lazyincrementalBuild

import { defineDevConfig } from '@rolldown/test-dev-server'; export default defineDevConfig({ build: { input: './src/entry-a.js', output: { strictExecutionOrder: true, }, experimental: { devMode: { lazy: true, }, incrementalBuild: true, }, treeshake: false, }, });

作用范围:只针对动态导入

Lazy compilation 有明确的边界:

  • 仅动态导入(import()—— 静态导入永远被立即编译,不参与懒编译;
  • 独立特性—— 复用 HMR 运行时/渲染路径产出模块输出;/@vite/lazy请求本身不会触发 HMR 更新。但一旦被 fetch,懒模块就变成普通的被监视(watched)图内模块,之后的编辑会走标准的按客户端 HMR 管线(详见 implementation.md "Editing a fetched lazy module");
  • 模块类型盲区(module-type-blind)边界——resolve_id会代理每一个动态导入,不做扩展名或模块类型过滤,因此真实目标直到第一次/@vite/lazy请求才被加载。编译单元内的一切都必须能渲染成 ECMAScript AST,由此带来几类边界情况:
    • CSS:Rolldown 已移除 CSS 打包能力(#4271),懒编译会把这个硬错误从"服务启动时"推迟到"第一次/lazy请求"(HTTP 500,可在消费方await import()处以可捕获的 rejection 形式收到);
    • JSON / text / base64 / dataurl:目前在懒 chunk 内是坏的——它们的导出在链接阶段合成,而懒渲染路径会跳过该步骤,导致首次加载时注册为空导出,需要等一次重建 + 页面刷新后才正常(详见 implementation.md Known Limitations);
    • 二进制资源:只有当某个插件在load钩子中把二进制转为 JS 时才能工作(例如 dev server 的 Vite 风格 asset 插件);编译期发射的字节通过onAdditionalAssets回调交付(#9815)。
  • 编译单元包含懒模块的所有静态依赖,new URL(...)引用也按静态依赖处理。

编译粒度:懒边界(Lazy Boundary)

当一个懒模块被请求时:

  • 只编译该模块 + 它的同步依赖,并减去请求方客户端已经执行过的模块(通过executed_modules做按客户端剪枝);
  • 懒模块内部的嵌套动态导入不会编译,它们会成为新的懒边界;
  • 这就在每个动态导入处天然形成了一层"懒边界"。
Entry ├── sync-dep-1 (compiled immediately) ├── sync-dep-2 (compiled immediately) └── import('./lazy-a') ← lazy boundary ├── sync-dep-3 (compiled when lazy-a is requested) ├── sync-dep-4 (compiled when lazy-a is requested) └── import('./lazy-b') ← another lazy boundary (NOT compiled yet)

在渲染出的懒 chunk(或 HMR patch)内部,嵌套的import()指向另一个懒代理时,HMR finalizer 会把它重写为先去 fetch/@vite/lazy?...,再通过loadExports(stableProxyId)读取代理注册的导出——因为 partial bundle 没有单独打包的代理 chunk,如果不这样读取,代理的顶层导出就会丢失(详见 implementation.md "Lazy chunk rendering")。

核心设计决策

1. 透明的用户体验

用户不需要改动任何代码。import('./module')直接可用:插件自动重写动态导入、自动解包代理导出。LazyCompilationPlugin只注册四个钩子(HookUsage::BuildStart | ResolveId | Load | TransformAst,见 lazy_compilation_plugin.rs),其中build_start只负责捕获cwd供后续计算稳定 ID 使用。

2.rolldown:exports契约

代理模块导出一个特殊命名导出'rolldown:exports'——它是一个Promise,解析为真实模块的导出;若真实模块初始化时抛错,它会reject,这正是初始化错误能在消费方await import()处被捕获的原因(#9981)。

Rolldown 的transform_ast钩子会自动把动态导入包上一层解包助手:

// User code (unchanged) const mod = await import('./lazy.js'); // Transformed by lazy compilation plugin const mod = await import('./lazy.js').then(__unwrap_lazy_compilation_entry);
  • 助手函数会在至少有一个动态导入被包裹的模块中注入,且插入在指令序言(directive prologue,如"use strict")之后以保留其语义:

    function __unwrap_lazy_compilation_entry(m) { var e = m['rolldown:exports']; return e ? e : m; }

    实际 AST 构建逻辑在 runtime_injector.rs:LazyCompilationRuntimeInjector遍历表达式,把每个ImportExpression重写为import(...).then(__unwrap_lazy_compilation_entry),并用transformed_count记录是否注入助手;create_unwrap_lazy_compilation_entry_helperfunction声明逐节点构造出上面的函数体(参数mvar e = m['rolldown:exports'];return e ? e : m;)。

  • 该变换对所有动态导入都安全:懒代理返回 promise,非懒模块则原样透传,e为空时助手直接返回模块命名空间;

  • 代理模块自身被豁免:ID 含?rolldown-lazy=1的模块不会被transform_ast处理,因此 stub 模板里的import('/@vite/lazy?...')和 fetched 模板里的import($MODULE_ID)永远不会被再次包裹(见transform_ast开头的if args.id.contains("?rolldown-lazy=1")早退)。

3. 代理模块的两种状态

代理模块有两个状态,决定LazyCompilationPluginload钩子返回什么内容:

未 fetch(初始状态)

返回stub 模板(proxy-module-template.js),它通过/@vite/lazy端点获取真实代码:

const lazyExports = (async () => { // Remove the cache of the current module from the runtime's module map. // This module with key $STABLE_PROXY_MODULE_ID is swapped in the lazy loaded chunk again with the real module. delete __rolldown_runtime__.modules[$STABLE_PROXY_MODULE_ID]; // Dev server will intercept this import and serve the actual module code. // We send the proxy module ID (with ?rolldown-lazy=1) so the server can mark it as fetched. await import( /* @vite-ignore */ `/@vite/lazy?id=${encodeURIComponent($PROXY_MODULE_ID)}&clientId=${__rolldown_runtime__.clientId}` ); // Loading the chunk re-registers this proxy id, exposing the real module's // initializer as its own `rolldown:exports` promise. Await that promise (don't // just hand back the namespace) so an error thrown while the real module // initializes rejects `lazyExports` too, surfacing at the consumer's // `await import(...)` (catchable) instead of escaping as an unhandled rejection. return await __rolldown_runtime__.loadExports($STABLE_PROXY_MODULE_ID)['rolldown:exports']; })(); export { lazyExports as 'rolldown:exports' };

这里包含三步:(1) 驱逐代理过期的运行时注册,让懒 chunk 能用同一个稳定代理 ID重新注册真实初始化器(当前实现已演化为调用__rolldown_runtime__.removeModuleCache($STABLE_PROXY_MODULE_ID),语义相同);(2) fetch 懒 chunk;(3) 通过重新注册后的代理自身的'rolldown:exports'promise解析——这是一个两层的 promise 链,其 rejection 语义使得初始化错误在消费方处可捕获。

已 fetch(首次请求之后)

返回fetched 模板(proxy-module-template-fetched.js),它直接导入真实模块:

const lazyExports = (async () => { await import($MODULE_ID); return __rolldown_runtime__.loadExports($STABLE_MODULE_ID); })(); export { lazyExports as 'rolldown:exports' };

动态导入的返回值(namespace)被有意丢弃:导出改为按稳定 ID 从运行时注册表读取,因为当一个共享懒模块落入公共 chunk 时,chunk 级重命名可能压缩导出名,直接取 namespace 会得到undefined(#9132)。其中$MODULE_ID是绝对路径(仅用于解析),$STABLE_MODULE_ID是相对 cwd 的稳定 ID。

状态转换由LazyCompilationContext.mark_as_fetched()管理(lazy_compilation_plugin.rs),DevEngine 在每次懒编译之前调用它(见 dev_engine.rs)。

两个模板共使用四个占位符,每个都被替换为 serde_json 引号包裹的 JS 字符串字面量(因此 Windows 反斜杠路径也能正确转义,#9102):

占位符使用位置
$PROXY_MODULE_ID绝对路径 +?rolldown-lazy=1stub ——/@vite/lazy?id=请求参数
$STABLE_PROXY_MODULE_ID稳定 ID +?rolldown-lazy=1stub —— 模块表 delete +loadExports
$MODULE_ID绝对路径(去掉 query)fetched ——import($MODULE_ID)
$STABLE_MODULE_ID稳定 IDfetched ——loadExports($STABLE_MODULE_ID)

render_proxy_template按"长的先替换"的顺序处理,且$MODULE_ID最后替换,因为其他三个占位符名都包含MODULE_ID子串(lazy_compilation_plugin.rs)。配套的单元测试覆盖了 Windows 与 Unix 路径的转义正确性(同文件tests模块)。

4. Dev Server 集成

Dev server 处理/@vite/lazy?id=...&clientId=...请求的完整流程:

  1. 收到携带代理模块 ID(绝对路径 +?rolldown-lazy=1)与客户端 UUID 的请求;
  2. 调用DevEngine.compileEntry(moduleId, clientId)(TS 侧)/DevEngine::compile_lazy_entry(Rust 侧);
  3. DevEngine 查询该客户端的executed_modules,并把代理标记为已 fetch;
  4. 安全门:该 ID 只是构建缓存的查找键——不在模块图中的 ID 会被以Lazy entry module not found in cache拒绝(绝不从文件系统解析,因此恶意请求无法打包任意文件;类似 Vite 的server.fs.strict,由测试钉死,见 dev-lazy-compile.test.ts,#9969)。注意顺序:DevEngine::compile_lazy_entry会在该校验之前无条件调用mark_as_fetched,因此未知 ID 仍会进入fetched_entries(无害,但排查问题时值得了解);
  5. 从代理模块做部分扫描(ScanMode::Partial)——插件返回 fetched 模板,其import($MODULE_ID)触发真实模块的编译;
  6. 编译期间发射的资源通过onAdditionalAssets回调在代码返回之前交付,保证 chunk 执行时资源已可服务(#9815,修复 vitejs/vite#22596);
  7. 直接返回编译后的 JSContent-Type: application/javascript),浏览器以 ES module 方式加载;编译失败则应答 HTTP 500;
  8. 通知协调器触发一次后台重建,使后续页面加载直接拿到 fetched 模板,无需再发/lazy请求。

resolve_id钩子中,插件处理两个关键分支(lazy_compilation_plugin.rs):

  • 已知代理 ID 的再解析(任何导入类型,例如 dev server 把 stub ID 当作入口来服务懒编译请求):若 specifier 以?rolldown-lazy=1结尾且在lazy_entries中,则解析为其自身;未知代理 ID 则继续不可解析(对应 #9969 安全门);
  • 动态导入且导入方是已 fetch 的代理:返回Ok(None),跳过代理创建,让import($MODULE_ID)正常解析到真实模块——否则会为同一模块再创建一个代理,造成无限递归(Issue 3 的经验教训);
  • 其余动态导入:通过ctx.resolveskip_self: true、转发custom)解析原 ID 后追加?rolldown-lazy=1。追加是幂等的(#9439):ctx.resolve可能重入其他插件的 resolve 钩子(如别名插件),若解析结果已带标记则直接复用,避免双重后缀导致 stub 模板中的运行时失效键与代理 ID 失配(回归问题 vitejs/vite#22454)。

load钩子则只服务lazy_entries中存在的代理 ID,其余?rolldown-lazy=1ID 一律放行返回Ok(None);且对代理 ID 会跳过用户侧所有构建钩子(resolve_idloadtransformtransform_astmodule_parsed),使用户插件只能看到真实模块。

数据生命周期

懒编译涉及两个作用域的数据:

Session 作用域(跨所有浏览器标签页共享,贯穿 dev server 整个生命周期)

数据说明
Module Graph所有已解析、已编译的模块
lazy_entries解析过程中发现的所有代理模块 ID 集合
fetched_entries已通过/@vite/lazy请求 fetch 过的代理模块集合
Build Output磁盘/内存中的打包 JS 文件
Watched Files被监视变更的文件

关键行为:一旦某个懒模块被任一客户端 fetch,之后所有客户端拿到的都是 fetched 模板(直接导入真实模块)。懒编译完成后构建输出会被刷新,因此后续页面加载无需/lazy请求即可拿到 fetched 模板。

Client 作用域(每个浏览器标签页独立,用clientId标识)

数据说明
clientId浏览器标签页的唯一标识
executed_modules浏览器实际执行过的模块(用于 HMR 边界计算与懒 patch 剪枝)
  • 会话生命周期:clientId → ClientSession { executed_modules }存于 DevEngine 的SharedClients;在收到该 clientId 的首条hmr:module-registered消息时隐式创建,websocket 断开时经removeClient移除;
  • executed_modules只增不减的稳定 ID 集合,且包含形如src/foo.js?rolldown-lazy=1的代理 ID——懒 chunk 会用稳定 ID 重新注册代理;
  • 特殊 clientId"rolldown-tests"被视为已执行一切(Rust 层测试绕过按客户端门控;只有浏览器 E2E playground 走executed_modules剪枝路径);
  • clientId由 HMR 运行时在初始化时通过crypto.randomUUID()生成,追加到 websocket URL 与每个/@vite/lazy请求的clientId参数中;它在懒编译中的唯一作用是按客户端剪枝,没有"路由"语义——编译结果在 HTTP 响应中同步返回;未知clientId静默退化为空执行集(返回完整依赖闭包)。

Fetched 与 Executed 的区分

这是两个作用域下的不同概念:

  • Fetched(会话级):浏览器对该代理模块发过/lazy请求,服务端已编译真实模块及其依赖,所有客户端此后都拿 fetched 模板;
  • Executed(客户端级):浏览器真正执行过该模块代码,用于剪枝某个客户端的懒 patch 并门控 HMR 传播。

两者可以不同步:客户端 A fetch 了某模块,客户端 B 可能还没导航到对应路由。当已 fetch 的懒模块被编辑时,各客户端结果不同:执行过它的客户端收到真正的Patch(若无 HMR 边界接受变更则FullReload);从未执行过的客户端收到一个内容为空但非NoopPatch(代码只是__rolldown_runtime__.applyUpdates([]);)。

构建输出刷新(后台重建)

懒编译成功后,DevEngine::compile_lazy_entry的成功分支依次做两件事(dev_engine.rs):

if result.is_ok() { // 1. 交付编译期间发射的资源(在代码返回前) if let Some(on_additional_assets) = ... { ... } // 2. 排队后台重建 self.notify_module_changed(proxy_module_id); }

协调器收到ModuleChanged(携带含?rolldown-lazy=1原始代理 ID)后:

  1. 先调用update_watch_paths()——否则懒编译过程中发现的新监视文件会在重建任务启动时被丢弃,这一步正是让"之后编辑懒模块能触发重建"的关键;
  2. 排队一个Rebuild任务,把代理 ID 当作变更文件,并把输出标记为 stale;
  3. 重建把构建输出中的 stub 换成 fetched 模板;之后的页面加载直接拿到它,无需/lazy请求。

原始代理 ID 被刻意不规范化:部分重建时它解析回自身(resolver 保留 query)、字符串匹配增量缓存中代理模块的键、并强制代理的load钩子重跑——这次返回 fetched 模板。若规范化为真实模块 ID,则会失效错误的模块并留下缓存的 stub 代理。

成功的后台重建对已连接客户端是静默的:输出原地替换、不发送任何 websocket 消息(正在运行的页面继续使用/lazy返回的代码);只有已有FullReload挂起或服务器正从先前广播的构建错误中恢复时才触发 reload。Rebuild任务从不产生 HMR 更新,只与其他Rebuild合并,因此?rolldown-lazy=1伪路径永远不会泄漏进 HMR 更新计算——不过插件会通过watch_change钩子观察到它一次。

失败路径:懒编译失败时两个步骤都不执行——不排队重建、stub 模板留在构建输出中(但代理仍保持已 fetch 标记)。若后台重建本身失败,消费方会缓存错误、向所有客户端广播错误浮层,并取消挂起的全量 reload,确保页面不会重载到损坏的 bundle 上(#9903)。

错误处理

当前错误契约(不再是早期 POC 阶段"Err 或 panic 都可以"):

  • 未知模块 IDErr("Lazy entry module not found in cache. module_id=...")(hmr_stage.rs 的compile_lazy_entry,此处即 #9969 安全门);napi 绑定层将其包装为带Failed to compile lazy entry: ...前缀的 rejected promise;dev-server middleware 应答 HTTP 500(缺失id/clientId参数时放行给next();成功时设置Content-Type: application/javascript);
  • 初始化错误可捕获(#9981):懒模块初始化抛错会使重新注册的代理的'rolldown:exports'promise reject,进而 reject stub 的lazyExports,最终在消费方await import(...)处抛错——try/catch生效;无处理程序时恰好触发一次unhandledrejection。冷路径(首次/lazy编译)与热路径(重建+刷新后的 fetched 代理)都有对应 spec 钉死;
  • 运行时loadExports未命中不抛错——仅告警并返回{}
  • 唯一遗留的 panic:在任何 bundle 构建之前调用compile_lazy_entry

已知限制

共享模块去重

当多个懒入口共享公共依赖时,两层机制协同防止重复执行:

Entry ├── import('./lazy-a') ← lazy boundary │ └── shared.js (sync dep) └── import('./lazy-b') ← lazy boundary └── shared.js (sync dep)
  1. 服务端剪枝:收集懒 patch 的同步依赖时,跳过其稳定 ID 在请求方客户端executed_modules中的模块(由hmr:module-registered填充);
  2. 运行时去重标志:懒 chunk 以dedup_module_initializer: true渲染,给每个模块包装器追加第三个真值参数——createEsmInitializer(stableId, factory, 1)/createCjsInitializer(...)——运行时在 ID 已注册时跳过 factory。

服务端仍存在竞态窗口(两次/lazy请求快速连发、首个 patch 的hmr:module-registered尚未到达时会产生重叠 chunk,hmr_stage.rs中有 TODO),但运行时去重标志使其无害:shared.js同时出现在两个 chunk 中却只执行一次。HMR patch 刻意省略去重标志dedup_module_initializer: false):patch 的意义就在于重新执行模块体并发布新导出,去重会静默丢弃更新。代码注释将该标志标记为在运行时 dispose/re-execute API 就绪前的临时方案。

链接期合成导出(JSON、text、base64、dataurl)

导出在链接阶段合成的模块(JSON/text/base64/dataurl)在懒 chunk 与 HMR patch 内都是坏的:它们被扫描为裸表达式语句(ExportsKind::None),export default仅由链接阶段的generate_lazy_export物化,而懒/HMR 渲染路径从不运行它(渲染的是纯扫描期 AST 克隆)。懒 chunk 以registerModule(id)注册它们,运行时填充为{ exports: {} }——因此导入方在首次懒加载时看到空导出;后台重建 + 页面刷新后完整构建应用了变换,同一导入才正常。目前尚无 playground fixture 覆盖此场景。

CSS

Rolldown 已移除 CSS 打包(#4271),且懒边界创建时不加载目标——所以import('./style.css')能构建成功,硬错误(Bundling CSS is no longer supported)被推迟到第一次/lazy请求:HTTP 500,消费方await import()处收到可捕获的 rejection。

二进制资源

Rolldown 核心没有内置资源处理:默认module_types映射之外的扩展名会被按 UTF-8 读取并当作 JS 解析,因此懒子树内静态导入的二进制文件会在请求时导致懒编译失败。懒子树内的资源导入只有在插件于load钩子中将其转为 JS 时才可用(dev server 移植的vite:asset插件正是如此)。

Sourcemaps

懒 chunk 只有sourcemap: 'inline'可用。/lazy负载在整条链路上(HmrStageDevEngine→ napi → middleware)都只是一个普通String,没有独立 map 文件的字段:用'file'/true时代码会带上//# sourceMappingURL=lazy_compile_{n}.js.map注释但 map 资源在服务端被丢弃(注释悬空);'hidden'则静默丢弃 map。相比之下,HMR patch 通过HmrPatch { sourcemap, sourcemap_filename }携带 map,dev server 从内存文件存储同时提供 patch 与 map——因此同一份sourcemap: 'file'配置对 HMR 编辑有效、对懒 chunk 静默失效。该路径目前无测试覆盖。

测试覆盖与验证

E2E playground 位于packages/test-dev-server/tests/playground/lazy-compilation/(一个统一 dev server 配置,experimental.devMode.lazy: true+ 一个别名插件),各场景独立目录、由main.js按需导入,确保每个 spec 都拿到"全新首次 fetch":

Spec钉死的行为
basic懒模块以两个独立 JS 请求到达(代理 chunk + 真实 chunk),见 basic.spec.ts
aliased-import别名重入下幂等的代理 ID 创建(vite#22454)
emitted-asset懒编译期间发射的资源在首次加载即可服务(vite#22596)
lazy-init-error初始化错误可用 try/catch 捕获——冷、热两条路径(#9975/#9981)
lazy-init-error-unhandled无处理程序时恰好一次unhandledrejection——冷、热路径
nested-dynamic-import懒 chunk 内嵌套的懒import()在首次点击即可解析
shared-module共享 chunk 中的导出名保持(#9132)+ fetch 后的 watch/自动 reload

多个 spec 使用retry: 0,因为这些 bug 只会在全新服务器的首次交互中复现。单元测试 dev-lazy-compile.test.ts 钉死了未知 ID 拒绝行为(#9969):先执行完整构建填充缓存,再调用engine.compileEntry('/does/not/exist.js?rolldown-lazy=1', 'some-client'),断言抛出Lazy entry module not found in cache

关键源码索引

便于后续深入阅读的源码路径:

  • 核心插件:lazy_compilation_plugin.rs(resolve_id/load/transform_astLazyCompilationContextrender_proxy_template);
  • AST 注入器:runtime_injector.rs(包裹动态导入、生成__unwrap_lazy_compilation_entry);
  • 双状态模板:proxy-module-template.js 与 proxy-module-template-fetched.js;
  • 内置插件注册:apply_inner_plugins.rs(experimental.dev_mode.lazy == true时注册);
  • Dev Engine:dev_engine.rs(compile_lazy_entry:executed_modules 查询、mark-as-fetched、资源交付、notify_module_changed);
  • HMR/构建:hmr_stage.rs(compile_lazy_entry:缓存门控、部分扫描、按客户端依赖收集、chunk 渲染),以及crates/rolldown/src/hmr/下的 finalizer 与 utils;
  • 示例:examples/lazy-compilation/dev.config.mjs 与examples/lazy-compilation/src/下的入口、异步依赖与共享模块。

结语

Rolldown 的懒编译把"编译什么"从构建期决策推迟到浏览器运行期,以"代理模块 + 双状态模板 +rolldown:exportspromise 契约"这一套组合拳实现了完全透明的按需编译:用户代码零改动,嵌套动态导入自动形成新的懒边界,共享模块通过服务端剪枝与运行时去重标志保证只执行一次,而/@vite/lazy请求只做缓存查找、绝不读文件系统的安全设计则堵住了任意文件打包的漏洞。开发者在 dev 模式下只需experimental: { devMode: { lazy: true } }一行配置即可获得更快的冷启动体验——同时需要留意 JSON/CSS/二进制资源与 sourcemap 在懒路径下的已知边界。

【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询