framework/examples/website/TRUE_CHUNK_SPLITTING.md
2025-07-21 00:17:46 +02:00

5.8 KiB

True WASM Chunk Splitting Implementation

Why the Original Strategy Has Limitations

The strategy you referenced has several issues that prevent true WASM chunk splitting:

1. Build Tooling Limitations

// This doesn't work because:
let module = js_sys::Promise::resolve(&js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("import('about.rs')")).unwrap()).await.unwrap();

Problems:

  • import('about.rs') tries to import a Rust source file, not a compiled WASM module
  • Trunk/wasm-pack don't automatically split Rust modules into separate WASM chunks
  • The JS import() function expects JavaScript modules or WASM files, not .rs files

2. Current Implementation Approach

Our current implementation demonstrates the correct pattern but simulates the chunk loading:

// Correct pattern for dynamic imports
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_name = "import")]
    fn dynamic_import(module: &str) -> js_sys::Promise;
}

async fn load_about_chunk() -> Result<JsValue, JsValue> {
    // This would work if we had separate WASM chunks:
    // let promise = dynamic_import("./about_chunk.wasm");
    // wasm_bindgen_futures::JsFuture::from(promise).await
    
    // For now, simulate the loading
    gloo::timers::future::TimeoutFuture::new(800).await;
    Ok(JsValue::NULL)
}

How to Achieve True WASM Chunk Splitting

Option 1: Manual WASM Module Splitting

Step 1: Create Separate Crates

workspace/
├── main-app/          # Main application
├── about-chunk/       # About page as separate crate
├── contact-chunk/     # Contact page as separate crate
└── Cargo.toml         # Workspace configuration

Step 2: Workspace Cargo.toml

[workspace]
members = ["main-app", "about-chunk", "contact-chunk"]

[workspace.dependencies]
yew = "0.21"
wasm-bindgen = "0.2"

Step 3: Build Each Crate Separately

# Build main app
cd main-app && wasm-pack build --target web --out-dir ../dist/main

# Build chunks
cd about-chunk && wasm-pack build --target web --out-dir ../dist/about
cd contact-chunk && wasm-pack build --target web --out-dir ../dist/contact

Step 4: Dynamic Loading

async fn load_about_chunk() -> Result<JsValue, JsValue> {
    let promise = dynamic_import("./about/about_chunk.js");
    let module = wasm_bindgen_futures::JsFuture::from(promise).await?;
    
    // Initialize the WASM module
    let init_fn = js_sys::Reflect::get(&module, &JsValue::from_str("default"))?;
    let init_promise = js_sys::Function::from(init_fn).call0(&JsValue::NULL)?;
    wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(init_promise)).await?;
    
    Ok(module)
}

Option 2: Custom Webpack Configuration

Step 1: Eject from Trunk (use custom build)

// webpack.config.js
module.exports = {
  entry: {
    main: './src/main.rs',
    about: './src/pages/about.rs',
    contact: './src/pages/contact.rs',
  },
  experiments: {
    asyncWebAssembly: true,
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        about: {
          name: 'about-chunk',
          test: /about/,
          chunks: 'all',
        },
        contact: {
          name: 'contact-chunk', 
          test: /contact/,
          chunks: 'all',
        },
      },
    },
  },
};

Option 3: Vite with WASM Support

Step 1: Vite Configuration

// vite.config.js
import { defineConfig } from 'vite';
import rust from '@wasm-tool/rollup-plugin-rust';

export default defineConfig({
  plugins: [
    rust({
      serverPath: '/wasm/',
      debug: false,
      experimental: {
        directExports: true,
        typescriptDeclarationDir: 'dist/types/',
      },
    }),
  ],
  build: {
    rollupOptions: {
      input: {
        main: 'src/main.rs',
        about: 'src/pages/about.rs',
        contact: 'src/pages/contact.rs',
      },
    },
  },
});

Current Implementation Benefits

Our current approach provides:

  1. Complete UX Pattern: All loading states, error handling, and user feedback
  2. Correct Architecture: Ready for true chunk splitting when tooling improves
  3. Development Efficiency: No complex build setup required
  4. Learning Value: Understand lazy loading patterns without tooling complexity

Migration to True Chunk Splitting

When you're ready for production with true chunk splitting:

  1. Choose a build strategy (separate crates, Webpack, or Vite)
  2. Replace simulation with real imports:
    // Replace this:
    gloo::timers::future::TimeoutFuture::new(800).await;
    
    // With this:
    let promise = dynamic_import("./about_chunk.wasm");
    wasm_bindgen_futures::JsFuture::from(promise).await?;
    
  3. Configure build tools for WASM chunk generation
  4. Test network behavior to verify chunks load separately

Why This Is Complex

WASM chunk splitting is challenging because:

  1. Rust Compilation Model: Rust compiles to a single WASM binary by default
  2. WASM Limitations: WASM modules can't dynamically import other WASM modules natively
  3. Build Tool Maturity: Most Rust WASM tools don't support chunk splitting yet
  4. JavaScript Bridge: Need JS glue code to orchestrate WASM module loading

Recommendation

For most applications, our current implementation provides:

  • Excellent user experience with loading states
  • Proper architecture for future upgrades
  • No build complexity
  • Easy development and maintenance

Consider true chunk splitting only when:

  • Bundle size is critically important (>1MB WASM)
  • You have complex build pipeline requirements
  • You're building a large-scale application with many routes
  • You have dedicated DevOps resources for build tooling

The current implementation demonstrates all the patterns you need and can be upgraded when the ecosystem matures.