From 7a5487efe7d346de82dab383587feee3a052d89b Mon Sep 17 00:00:00 2001
From: Martin Molzer
Date: Tue, 7 Oct 2025 03:15:18 +0200
Subject: [PATCH 01/10] initial (very incomplete) integration of split-wasm
tooling
---
Cargo.lock | 54 ++++++++
examples/split-wasm/Cargo.toml | 16 +++
examples/split-wasm/build.sh | 44 +++++++
examples/split-wasm/index.html | 28 ++++
examples/split-wasm/src/main.rs | 20 +++
examples/split-wasm/src/yew.rs | 79 +++++++++++
packages/yew/src/html/component/mod.rs | 20 +++
packages/yew/src/lazy.rs | 173 +++++++++++++++++++++++++
packages/yew/src/lib.rs | 1 +
9 files changed, 435 insertions(+)
create mode 100644 examples/split-wasm/Cargo.toml
create mode 100755 examples/split-wasm/build.sh
create mode 100644 examples/split-wasm/index.html
create mode 100644 examples/split-wasm/src/main.rs
create mode 100644 examples/split-wasm/src/yew.rs
create mode 100644 packages/yew/src/lazy.rs
diff --git a/Cargo.lock b/Cargo.lock
index 9ee1f740c66..d2f8c36ba16 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,6 +82,12 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+[[package]]
+name = "async-once-cell"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -187,6 +193,12 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "base16"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8"
+
[[package]]
name = "base64"
version = "0.22.1"
@@ -2909,6 +2921,17 @@ dependencies = [
"digest",
]
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2981,6 +3004,17 @@ dependencies = [
"lock_api",
]
+[[package]]
+name = "split-wasm"
+version = "0.1.0"
+dependencies = [
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm_split",
+ "web-sys",
+ "yew",
+]
+
[[package]]
name = "ssr-e2e"
version = "0.1.0"
@@ -3880,6 +3914,26 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wasm_split"
+version = "0.1.0"
+source = "git+https://github.com/WorldSEnder/wasm-split-prototype.git?rev=9b4876034a3be928a634c55be9c55085a9395f46#9b4876034a3be928a634c55be9c55085a9395f46"
+dependencies = [
+ "async-once-cell",
+ "wasm_split_macros",
+]
+
+[[package]]
+name = "wasm_split_macros"
+version = "0.1.0"
+source = "git+https://github.com/WorldSEnder/wasm-split-prototype.git?rev=9b4876034a3be928a634c55be9c55085a9395f46#9b4876034a3be928a634c55be9c55085a9395f46"
+dependencies = [
+ "base16",
+ "quote",
+ "sha2",
+ "syn 2.0.117",
+]
+
[[package]]
name = "web-sys"
version = "0.3.91"
diff --git a/examples/split-wasm/Cargo.toml b/examples/split-wasm/Cargo.toml
new file mode 100644
index 00000000000..06735b6e521
--- /dev/null
+++ b/examples/split-wasm/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "split-wasm"
+version = "0.1.0"
+authors = []
+edition = "2021"
+license = "MIT OR Apache-2.0"
+
+[dependencies]
+yew = { path = "../../packages/yew", features = ["csr"] }
+wasm_split = { git = "https://github.com/WorldSEnder/wasm-split-prototype.git", version = "0.1", rev = "9b4876034a3be928a634c55be9c55085a9395f46" }
+wasm-bindgen = "*"
+wasm-bindgen-futures = "*"
+
+[dependencies.web-sys]
+version = "0.3"
+features = ["HtmlInputElement"]
diff --git a/examples/split-wasm/build.sh b/examples/split-wasm/build.sh
new file mode 100755
index 00000000000..78fb7536275
--- /dev/null
+++ b/examples/split-wasm/build.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -e
+shopt -s extglob
+
+CARGO="cargo"
+WASM_BINDGEN=~/.cache/trunk/"$(cargo tree --package wasm-bindgen --depth=0 --format="{p}" -e normal | sed -e 's/ v/-/g')/wasm-bindgen"
+echo "$WASM_BINDGEN"
+WASM_OPT="$(ls -dv1 ~/.cache/trunk/wasm-opt-version_* | tail -n 1)"/bin/wasm-opt # will select most recently installed :)
+
+PROFILE="release"
+THIS_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
+TARGET_DIR=$(cd -- "$THIS_DIR"/../../target/ &> /dev/null && pwd)
+OPT=1
+
+RUSTFLAGS="-Clink-args=--emit-relocs$(case "$CARGO" in *nightly*) echo " -Zunstable-options -Cpanic=immediate-abort" ;; esac)" \
+ $CARGO build --target wasm32-unknown-unknown \
+ $(case $PROFILE in "debug") ;; "release") echo "--release" ;; *) echo '--profile "${PROFILE}"' ;; esac)
+
+mkdir -p dist/
+GLOBIGNORE=".:.."
+rm -rf dist/*
+mkdir dist/.stage
+(
+ cd ~/recreational/src/wasm-split-prototype/
+ cargo run --features="build-binary" -- --verbose "$TARGET_DIR/wasm32-unknown-unknown/${PROFILE}/split-wasm.wasm" "$THIS_DIR"/dist/.stage/ \
+ > "$THIS_DIR"/dist/.stage/split.log
+)
+echo "running wasm-bindgen"
+$WASM_BINDGEN dist/.stage/main.wasm --out-dir dist/.stage --no-demangle --target web --keep-lld-exports --no-typescript
+if [ "$OPT" == 1 ] ; then
+ echo "running wasm-opt"
+ for wasm in dist/.stage/!(main).wasm ; do
+ $WASM_OPT -Os "$wasm" -o dist/"$(basename -- "$wasm")"
+ done
+else
+ for wasm in dist/.stage/!(main).wasm ; do
+ mv "$wasm" dist/"$(basename -- "$wasm")"
+ done
+fi
+echo "moving to dist dir"
+mv dist/.stage/*.!(wasm) dist
+#rmdir dist/.stage
+cp index.html dist/
+
diff --git a/examples/split-wasm/index.html b/examples/split-wasm/index.html
new file mode 100644
index 00000000000..6d96bbcd761
--- /dev/null
+++ b/examples/split-wasm/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Yew • Split WASM
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/split-wasm/src/main.rs b/examples/split-wasm/src/main.rs
new file mode 100644
index 00000000000..4e6c354bc56
--- /dev/null
+++ b/examples/split-wasm/src/main.rs
@@ -0,0 +1,20 @@
+use std::cell::Cell;
+
+use wasm_bindgen::prelude::wasm_bindgen;
+
+// You can use a global variable from javascript, or a static
+// and even thread local variable without any changes.
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(thread_local_v2, js_name = "globalFoo")]
+ static GLOBAL_FOO: u32;
+}
+thread_local! {
+ static COUNTER: Cell = const { Cell::new(0) };
+}
+
+mod yew;
+
+pub fn main() {
+ yew::main();
+}
diff --git a/examples/split-wasm/src/yew.rs b/examples/split-wasm/src/yew.rs
new file mode 100644
index 00000000000..ae2ecb366fd
--- /dev/null
+++ b/examples/split-wasm/src/yew.rs
@@ -0,0 +1,79 @@
+use std::future::pending;
+
+use wasm_split::wasm_split as split;
+use web_sys::HtmlInputElement;
+use yew::lazy::{Lazy, LazyComponent, LazyVTable};
+use yew::prelude::*;
+use yew::suspense::Suspension;
+use yew::Renderer;
+
+use super::{COUNTER, GLOBAL_FOO};
+
+// ---------------------------------------------------------------------------
+// A counter component — the one we'll load lazily.
+// Uses use_state, which triggers re-renders via the scope it was created with.
+// ---------------------------------------------------------------------------
+
+#[derive(Clone, Debug, PartialEq, Properties)]
+pub struct CounterProps {
+ pub label: AttrValue,
+}
+
+#[component]
+fn Counter(props: &CounterProps) -> Html {
+ let count = use_state(|| 0_i32);
+ let onclick = {
+ let count = count.clone();
+ Callback::from(move |_| count.set(*count + 1))
+ };
+ let global_foo = GLOBAL_FOO.with(|f| *f);
+ let render_counter = COUNTER.with(|cnt| {
+ let c = cnt.get();
+ cnt.set(c + 1);
+ c
+ });
+
+ html! {
+
+
{"This component is loaded from a separate bundle, render count: "}{render_counter}
+
{"Here is a number loaded from (shared) memory: "}{global_foo}
+
{ &props.label }
+
{ *count }
+
+
+ }
+}
+
+impl LazyComponent for Counter {
+ async fn fetch() -> LazyVTable {
+ #[split(lazy_addition)]
+ fn split_fetch() -> LazyVTable {
+ LazyVTable::::vtable()
+ }
+ split_fetch().await
+ }
+}
+
+#[component]
+fn Pending() -> HtmlResult {
+ Err(Suspension::from_future(pending()).into())
+}
+
+#[component]
+fn App() -> Html {
+ let toggle = use_state(|| false);
+ let show = *toggle;
+ html! {
+ <>
+ ().checked())} />
+
+ {"not yet loaded"}
}}>
+ if show { label="A lazily loaded counter" /> } else { }
+
+ >
+ }
+}
+
+pub fn main() {
+ let _ = Renderer::::new().render();
+}
diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs
index c9bb5ad1de5..97acab24fea 100644
--- a/packages/yew/src/html/component/mod.rs
+++ b/packages/yew/src/html/component/mod.rs
@@ -68,6 +68,26 @@ impl Context {
state
}
+
+ pub(crate) fn narrow_scope>(
+ &self,
+ inner: &Scope,
+ ) -> Context {
+ let mut ctx = Context {
+ scope: inner.clone(),
+ props: self.props.clone(),
+ #[cfg(feature = "hydration")]
+ creation_mode: self.creation_mode,
+ #[cfg(feature = "hydration")]
+ prepared_state: None,
+ };
+ let _ = &mut ctx; // silence warning due to feature conditional branch below
+ #[cfg(feature = "hydration")]
+ {
+ ctx.prepared_state = self.prepared_state.clone();
+ }
+ ctx
+ }
}
/// The common base of both function components and struct components.
diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs
new file mode 100644
index 00000000000..3b4aa134f00
--- /dev/null
+++ b/packages/yew/src/lazy.rs
@@ -0,0 +1,173 @@
+//! Implements lazy fetching of components
+
+// A simple wrapper is easy to implement. This module exists to support message passing and more
+// involved logic
+
+use std::future::Future;
+
+use crate::html::{Scope, Scoped};
+use crate::suspense::Suspension;
+use crate::{BaseComponent, Context, HtmlResult};
+
+// we might be able to erase the BaseComponent bound here, then monomorphize for some size savings
+/// This struct is (mentally) the same as a `dyn BaseComponent` but without informing the linker
+/// about this.
+#[derive(Debug)]
+#[repr(C)]
+struct CompVTableImpl {
+ create: fn(&Context) -> C,
+ update: fn(&mut C, &Context, C::Message) -> bool,
+ changed: fn(&mut C, &Context, &C::Properties) -> bool,
+ view: fn(&C, &Context) -> HtmlResult,
+ rendered: fn(&mut C, &Context, bool),
+ destroy: fn(&mut C, &Context),
+ prepare_state: fn(&C) -> Option,
+ //...
+}
+/// Component vtable for a component.
+///
+/// Return `LazyVTable::::vtable()` from your implementation of
+/// [`LazyComponent::fetch`] after resolving it.
+pub struct LazyVTable {
+ imp: &'static CompVTableImpl,
+}
+impl std::fmt::Debug for LazyVTable {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("LazyVTable")
+ .field("vtable", &"...")
+ .finish()
+ }
+}
+impl LazyVTable {
+ /// Returns the singleton vtable for a component.
+ ///
+ /// Return this from [`LazyComponent::fetch`] for your lazy component.
+ pub fn vtable() -> LazyVTable {
+ LazyVTable {
+ imp: &const {
+ CompVTableImpl {
+ create: C::create,
+ update: C::update,
+ changed: C::changed,
+ view: C::view,
+ rendered: C::rendered,
+ destroy: C::destroy,
+ prepare_state: C::prepare_state,
+ }
+ },
+ }
+ }
+}
+/// Implement this trait to support lazily loading your component.
+///
+/// Used in conjunction with the [`Lazy`] component.
+pub trait LazyComponent: BaseComponent {
+ /// Start fetching your component
+ fn fetch() -> impl Future