notes/architecture/10-WASM-SPLIT.md
The WASM-Split system enables code splitting for large WebAssembly applications, allowing developers to lazily load feature chunks on demand.
Large WASM binaries can impact initial load times. WASM-Split produces:
dist/
├── main.wasm # Core application
├── module_1_*.wasm # Feature chunk 1
├── module_2_*.wasm # Feature chunk 2
├── chunk_1_*.wasm # Shared code
└── __wasm_split.js # JavaScript loader
#[wasm_split] Attribute MacroMarks async functions as split boundaries:
#[wasm_split(my_feature)]
async fn load_feature() -> i32 {
// This code will be in module_my_feature.wasm
expensive_computation()
}
Generated Names (pattern: __wasm_split_00<module>00_<type>_<hash>_<function>):
__wasm_split_00my_feature00_import_<hash>_load_feature - FFI import__wasm_split_00my_feature00_export_<hash>_load_feature - FFI exportTransformation:
extern "C" export with implementation__wasm_split.jsLazySplitLoader with load function#[lazy_loader] MacroFor libraries creating lazy-loadable wrappers:
#[lazy_loader(extern "auto")]
fn my_lazy_fn(x: i32) -> i32;
Returns LazyLoader<Args, Ret> with:
.load().await - Async module loading.call(args) - Sync invocation after loading"auto" ABI: Automatically combines all modules into one.
CLI takes two binary inputs:
--emit-relocs)Compilation requirements:
--emit-relocs - Relocation informationPhase 1: Discovery and Graph Building
1. Scan imports/exports for __wasm_split_00<module>00_* pattern
2. Parse relocations from original.wasm
3. Build call graph from:
- CODE section relocations
- DATA section relocations
- Direct function calls via IR walking
4. Build parent graph (inverse for reachability)
5. Compute reachability for each split point
Phase 2: Chunk Identification
Phase 3: Module Emission
Three output types (parallel via rayon):
A. Main Module:
B. Split Modules (per split point):
C. Chunk Modules (shared code):
The system uses walrus for WASM binary manipulation:
1. Ensure funcref table exists (__indirect_function_table)
2. Expand table for split modules + shared functions
3. Create passive element segments for initialization
Stubs perform indirect calls:
1. Push function arguments onto stack
2. Push table index (pointing to real function)
3. Call via CallIndirect with table
__wasm_split namespace for shared importsNode Types:
Node::Function(FunctionId)Node::DataSymbol(usize)Graphs:
pub struct LazyLoader<Args, Ret> {
// Generic loader for function (Args) -> Ret
}
impl LazyLoader {
pub async fn load(&self) -> bool; // Load module
pub fn call(&self, args: Args) -> Result<Ret, SplitLoaderError>;
}
Three states:
Async Interface:
SplitLoaderFuture implements Future<Output = bool>poll() handles state transitionsWaker to resume on callback__wasm_split.js)makeLoad() Function:
async function(callbackIndex, callbackData) {
// 1. Await chunk dependencies
// 2. Check if already loaded
// 3. Fetch module binary
// 4. Call initSync from main.wasm
// 5. Construct import object:
// - Memory from main module
// - Indirect function table
// - Stack pointers and TLS base
// - Main module exports as imports
// - Fused imports from other modules
// 6. WebAssembly.instantiateStreaming()
// 7. Add exports to fusedImports
// 8. Invoke callback with table index
}
Callback Mechanism:
__indirect_function_table.get(callbackIndex)(callbackData, true)
Wakes up Rust Future waiting in loader.
Compile Phase: Rust code with #[wasm_split]
↓
Macro Expansion: FFI functions + LazyLoader
↓
Build Phase: wasm-split CLI processes binaries
↓
Parse Relocations → Build Call Graph → Identify Split Points
↓
Compute Reachability → Parallel Emission
↓
Output: main.wasm + module_*.wasm + chunk_*.wasm + __wasm_split.js
↓
Runtime: JavaScript fetch → Instantiate → Callback → Future wakes
↓
Lazy functions available synchronously
// Define lazy-loadable feature
#[wasm_split(admin_panel)]
async fn load_admin_panel() -> AdminPanel {
AdminPanel::new()
}
// Use in application
async fn handle_route(route: Route) {
match route {
Route::Admin => {
let panel = load_admin_panel().await;
panel.render();
}
_ => // ...
}
}
#[wasm_split] per feature chunkpub enum SplitLoaderError {
NotLoaded,
LoadFailed,
}
Loader returns Result for robust error handling.