internal-docs/lazy-compilation/design.md
The implementation — data lifecycle, module-ID handling, end-to-end flow, and lessons learned: see implementation.md.
import('./module') just works, no user code changes (future goal)rolldown:exports contract - proxy modules export this; POC uses lazyMagic helper, later Rolldown runtime will unwrap automaticallyimport() become new lazy boundaries/lazy request returns compiled code, browser loads it as an ES modulemodule.id) consistently throughout the runtimeErr or panic is fine for POCLazy compilation is a development optimization that defers compilation of dynamically imported modules until they are actually requested at runtime.
import() is compiled just-in-time when the browser executes itimport('./foo') should just workimport()) - static imports are always compiledWhen a lazy module is requested:
import() within the lazy module) are not compiled - they become their own lazy boundariesEntry
├── 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)
Users should not need to change their code. import('./module') just works.
rolldown:exports ContractProxy modules export a special named export 'rolldown:exports':
// Proxy module for lazy ./foo.js (NOT EXECUTED state)
const lazyExports = (async () => {
await import(`/@vite/lazy?id=${encodeURIComponent($PROXY_MODULE_ID)}&clientId=...`);
return __rolldown_runtime__.loadExports($MODULE_ID);
})();
export { lazyExports as 'rolldown:exports' };
'rolldown:exports' is a promise that resolves to the real module's exports
Rolldown's transform_ast hook automatically wraps all dynamic imports with an unwrapping helper:
// 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);
The helper is injected into each module that has dynamic imports:
function __unwrap_lazy_compilation_entry(m) {
var e = m['rolldown:exports'];
return e ? e : m;
}
This is safe for ALL dynamic imports: lazy modules return the promise, non-lazy modules pass through unchanged
A proxy module has two states that determine what content the LazyCompilationPlugin returns:
Returns the stub template that fetches via /lazy endpoint:
// proxy-module-template.js
const lazyExports = (async () => {
await import(
`/@vite/lazy?id=${encodeURIComponent($PROXY_MODULE_ID)}&clientId=${__rolldown_runtime__.clientId}`
);
return __rolldown_runtime__.loadExports($MODULE_ID);
})();
export { lazyExports as 'rolldown:exports' };
Returns the fetched template that directly imports the real module:
// proxy-module-template-fetched.js
const lazyExports = (async () => {
const mod = await import($MODULE_ID);
return mod;
})();
export { lazyExports as 'rolldown:exports' };
The state transition is managed by LazyCompilationContext.mark_as_fetched().
The dev server handles /@vite/lazy?id=...&clientId=... requests:
/abs/path/foo.js?rolldown-lazy=1)DevEngine.compile_lazy_entry(proxyModuleId, clientId) (Rust) / DevEngine.compileEntry(moduleId, clientId) (TS)import($MODULE_ID) triggers compilation of actual module