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> + Send; +} + +#[derive(Debug)] +enum LazyState { + Pending(Suspension), + Created(LazyVTable, C), +} +/// Wrapper for a lazily fetched component +/// +/// This component suspends as long as the underlying component is still being fetched, +/// then behaves as the underlying component itself. +#[derive(Debug)] +pub struct Lazy { + inner_scope: Scope, + state: LazyState, +} + +/// Message to send to a lazy component +#[derive(Debug)] +pub enum LazyMessage { + /// Forward the message to the underlying component once that is fetched + Forward(C::Message), + #[doc(hidden)] + FetchFinished(LazyVTable, C), +} + +impl BaseComponent for Lazy { + type Message = LazyMessage; + type Properties = C::Properties; + + fn create(ctx: &Context) -> Self { + let inner_scope = Scope::new(Some(ctx.link().to_any())); + let creation_ctx = ctx.narrow_scope(&inner_scope); + + let link = ctx.link().clone(); + // TODO: fetch once per component type not instance? + let suspension = Suspension::from_future(async move { + // Ignore error in case receiver was dropped + let vtable = C::fetch().await; + let comp = (vtable.imp.create)(&creation_ctx); + link.send_message(LazyMessage::FetchFinished(vtable, comp)); + }); + Self { + inner_scope, + state: LazyState::Pending(suspension), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + let msg = match msg { + LazyMessage::FetchFinished(vtable, comp) => { + self.state = LazyState::Created(vtable, comp); + return true; + } + LazyMessage::Forward(msg) => msg, + }; + match &mut self.state { + LazyState::Pending(_) => { + // has a queueing implementation. We don't rerender until the suspension resolves + self.inner_scope.send_message(msg); + false + } + LazyState::Created(vtable, comp) => { + (vtable.imp.update)(comp, &ctx.narrow_scope(&self.inner_scope), msg) + } + } + } + + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + if let LazyState::Created(vtable, comp) = &mut self.state { + (vtable.imp.changed)(comp, &ctx.narrow_scope(&self.inner_scope), old_props) + } else { + false + } + } + + fn view(&self, ctx: &Context) -> crate::HtmlResult { + match &self.state { + LazyState::Pending(suspension) => Err(suspension.clone().into()), + LazyState::Created(vtable, comp) => { + (vtable.imp.view)(comp, &ctx.narrow_scope(&self.inner_scope)) + } + } + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + if let LazyState::Created(vtable, comp) = &mut self.state { + (vtable.imp.rendered)(comp, &ctx.narrow_scope(&self.inner_scope), first_render) + } else { + unreachable!("can't get rendered before fetching the vtable") + } + } + + fn destroy(&mut self, ctx: &Context) { + if let LazyState::Created(vtable, comp) = &mut self.state { + (vtable.imp.destroy)(comp, &ctx.narrow_scope(&self.inner_scope)) + } + } + + fn prepare_state(&self) -> Option { + if let LazyState::Created(vtable, comp) = &self.state { + (vtable.imp.prepare_state)(comp) + } else { + None + } + } +} diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index e6aff0a953b..6f4ee76c7c2 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -276,6 +276,7 @@ pub mod context; mod dom_bundle; pub mod functional; pub mod html; +pub mod lazy; pub mod platform; pub mod scheduler; mod sealed; From cc16c019313c64e3f509ad3767ed51f18438a756 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 14 Oct 2025 22:37:01 +0200 Subject: [PATCH 02/10] update to release candidate and improve caching don't start fetching once per component, cache the vtable. --- Cargo.lock | 15 +++++++++------ examples/split-wasm/Cargo.toml | 3 ++- examples/split-wasm/src/yew.rs | 35 ++++++++++++++++++++++++++++------ packages/yew/src/lazy.rs | 28 +++++++++++++++++---------- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2f8c36ba16..1fcdf56b7bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3008,9 +3008,10 @@ dependencies = [ name = "split-wasm" version = "0.1.0" dependencies = [ + "async-once-cell", "wasm-bindgen", "wasm-bindgen-futures", - "wasm_split", + "wasm_split_helpers", "web-sys", "yew", ] @@ -3915,9 +3916,10 @@ dependencies = [ ] [[package]] -name = "wasm_split" -version = "0.1.0" -source = "git+https://github.com/WorldSEnder/wasm-split-prototype.git?rev=9b4876034a3be928a634c55be9c55085a9395f46#9b4876034a3be928a634c55be9c55085a9395f46" +name = "wasm_split_helpers" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a63f86bb241fa35d6e87c50311e1abb9e403c5db690f3637525d6fa2876b8e" dependencies = [ "async-once-cell", "wasm_split_macros", @@ -3925,8 +3927,9 @@ dependencies = [ [[package]] name = "wasm_split_macros" -version = "0.1.0" -source = "git+https://github.com/WorldSEnder/wasm-split-prototype.git?rev=9b4876034a3be928a634c55be9c55085a9395f46#9b4876034a3be928a634c55be9c55085a9395f46" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee3692283763ee89e2eb075c51fff63c6385d6a31bcb1931262dc44180e58fb" dependencies = [ "base16", "quote", diff --git a/examples/split-wasm/Cargo.toml b/examples/split-wasm/Cargo.toml index 06735b6e521..61f8c168173 100644 --- a/examples/split-wasm/Cargo.toml +++ b/examples/split-wasm/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] +async-once-cell = "0.5.3" yew = { path = "../../packages/yew", features = ["csr"] } -wasm_split = { git = "https://github.com/WorldSEnder/wasm-split-prototype.git", version = "0.1", rev = "9b4876034a3be928a634c55be9c55085a9395f46" } +wasm_split_helpers = "0.2.0-rc.1" wasm-bindgen = "*" wasm-bindgen-futures = "*" diff --git a/examples/split-wasm/src/yew.rs b/examples/split-wasm/src/yew.rs index ae2ecb366fd..2984e861b2f 100644 --- a/examples/split-wasm/src/yew.rs +++ b/examples/split-wasm/src/yew.rs @@ -1,6 +1,7 @@ -use std::future::pending; +use std::future::{pending, Future}; +use std::pin::Pin; -use wasm_split::wasm_split as split; +use wasm_split_helpers::wasm_split as split; use web_sys::HtmlInputElement; use yew::lazy::{Lazy, LazyComponent, LazyVTable}; use yew::prelude::*; @@ -44,16 +45,38 @@ fn Counter(props: &CounterProps) -> Html { } } -impl LazyComponent for Counter { - async fn fetch() -> LazyVTable { +struct LazyAdditionProxy; + +impl LazyComponent for LazyAdditionProxy { + type Underlying = Counter; + + async fn fetch() -> LazyVTable { #[split(lazy_addition)] fn split_fetch() -> LazyVTable { LazyVTable::::vtable() } - split_fetch().await + struct F(Option> + Send>>>); + impl Future for F { + type Output = LazyVTable; + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + self.0 + .get_or_insert_with(|| Box::pin(split_fetch())) + .as_mut() + .poll(cx) + } + } + static CACHE: async_once_cell::Lazy, F> = + async_once_cell::Lazy::new(F(None)); + *Pin::static_ref(&CACHE).await.get_ref() } } +type LazyAddition = Lazy; + #[component] fn Pending() -> HtmlResult { Err(Suspension::from_future(pending()).into()) @@ -68,7 +91,7 @@ fn App() -> Html { ().checked())} /> {"not yet loaded"}

}}> - if show { label="A lazily loaded counter" /> } else { } + if show { } else { }
} diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index 3b4aa134f00..eb3623e67c0 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -38,6 +38,13 @@ impl std::fmt::Debug for LazyVTable { .finish() } } +impl Clone for LazyVTable { + fn clone(&self) -> Self { + *self + } +} +impl Copy for LazyVTable {} + impl LazyVTable { /// Returns the singleton vtable for a component. /// @@ -58,12 +65,14 @@ impl LazyVTable { } } } -/// Implement this trait to support lazily loading your component. +/// Implement this trait to support lazily loading a component. /// /// Used in conjunction with the [`Lazy`] component. -pub trait LazyComponent: BaseComponent { - /// Start fetching your component - fn fetch() -> impl Future> + Send; +pub trait LazyComponent: 'static { + /// The component that is lazily being fetched + type Underlying: BaseComponent; + /// Fetch the component's impl + fn fetch() -> impl Future> + Send; } #[derive(Debug)] @@ -76,9 +85,9 @@ enum LazyState { /// This component suspends as long as the underlying component is still being fetched, /// then behaves as the underlying component itself. #[derive(Debug)] -pub struct Lazy { - inner_scope: Scope, - state: LazyState, +pub struct Lazy { + inner_scope: Scope, + state: LazyState, } /// Message to send to a lazy component @@ -91,15 +100,14 @@ pub enum LazyMessage { } impl BaseComponent for Lazy { - type Message = LazyMessage; - type Properties = C::Properties; + type Message = LazyMessage; + type Properties = ::Properties; fn create(ctx: &Context) -> Self { let inner_scope = Scope::new(Some(ctx.link().to_any())); let creation_ctx = ctx.narrow_scope(&inner_scope); let link = ctx.link().clone(); - // TODO: fetch once per component type not instance? let suspension = Suspension::from_future(async move { // Ignore error in case receiver was dropped let vtable = C::fetch().await; From 6816229d9c9d45b65362d890b94f7b34848412a6 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 14 Oct 2025 22:57:13 +0200 Subject: [PATCH 03/10] add helper macro for lazy loading --- Cargo.lock | 1 + examples/split-wasm/src/yew.rs | 35 ++---------------------- packages/yew/Cargo.toml | 1 + packages/yew/src/lazy.rs | 50 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fcdf56b7bb..9a53fc1e65f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4291,6 +4291,7 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" name = "yew" version = "0.23.0" dependencies = [ + "async-once-cell", "base64ct", "bincode 2.0.0-rc.3", "console_error_panic_hook", diff --git a/examples/split-wasm/src/yew.rs b/examples/split-wasm/src/yew.rs index 2984e861b2f..3750dcd0339 100644 --- a/examples/split-wasm/src/yew.rs +++ b/examples/split-wasm/src/yew.rs @@ -1,9 +1,8 @@ use std::future::{pending, Future}; -use std::pin::Pin; use wasm_split_helpers::wasm_split as split; use web_sys::HtmlInputElement; -use yew::lazy::{Lazy, LazyComponent, LazyVTable}; +use yew::lazy::declare_lazy_component; use yew::prelude::*; use yew::suspense::Suspension; use yew::Renderer; @@ -45,37 +44,7 @@ fn Counter(props: &CounterProps) -> Html { } } -struct LazyAdditionProxy; - -impl LazyComponent for LazyAdditionProxy { - type Underlying = Counter; - - async fn fetch() -> LazyVTable { - #[split(lazy_addition)] - fn split_fetch() -> LazyVTable { - LazyVTable::::vtable() - } - struct F(Option> + Send>>>); - impl Future for F { - type Output = LazyVTable; - - fn poll( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - self.0 - .get_or_insert_with(|| Box::pin(split_fetch())) - .as_mut() - .poll(cx) - } - } - static CACHE: async_once_cell::Lazy, F> = - async_once_cell::Lazy::new(F(None)); - *Pin::static_ref(&CACHE).await.get_ref() - } -} - -type LazyAddition = Lazy; +declare_lazy_component!(Counter as LazyAddition in lazy_addition); #[component] fn Pending() -> HtmlResult { diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 36c4f538147..7a204cbbddc 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -34,6 +34,7 @@ serde = { workspace = true, features = ["derive"] } tracing = "0.1.44" tokise = "0.2.0" rustversion.workspace = true +async-once-cell = "0.5.3" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures.workspace = true diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index eb3623e67c0..4b01b80e5fe 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -179,3 +179,53 @@ impl BaseComponent for Lazy { } } } + +/// Make a component accessible as a lazily loaded component in a separate wasm module +#[doc(hidden)] +#[macro_export] +macro_rules! __declare_lazy_component { + ($comp:ty as $lazy_name:ident in $module:ident) => { + struct Proxy; + impl ::yew::lazy::LazyComponent for Proxy { + type Underlying = $comp; + + async fn fetch() -> ::yew::lazy::LazyVTable { + #[split($module)] + fn split_fetch() -> ::yew::lazy::LazyVTable<$comp> { + ::yew::lazy::LazyVTable::<$comp>::vtable() + } + struct F( + ::std::option::Option< + ::std::pin::Pin< + ::std::boxed::Box< + dyn ::std::future::Future> + + ::std::marker::Send, + >, + >, + >, + ); + impl Future for F { + type Output = ::yew::lazy::LazyVTable<$comp>; + + fn poll( + mut self: ::std::pin::Pin<&mut Self>, + cx: &mut ::std::task::Context<'_>, + ) -> ::std::task::Poll { + self.0 + .get_or_insert_with(|| ::std::boxed::Box::pin(split_fetch())) + .as_mut() + .poll(cx) + } + } + static CACHE: ::yew::lazy::LazyCell<::yew::lazy::LazyVTable<$comp>, F> = + ::yew::lazy::LazyCell::new(F(None)); + *::std::pin::Pin::static_ref(&CACHE).await.get_ref() + } + } + type $lazy_name = ::yew::lazy::Lazy; + }; +} +#[doc(hidden)] +pub use ::async_once_cell::Lazy as LazyCell; + +pub use crate::__declare_lazy_component as declare_lazy_component; From 8abb6566cc08c1c8d610ea92c5d78c12f23a6bce Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 16 Oct 2025 15:12:47 +0200 Subject: [PATCH 04/10] integrate wasm_split_helpers directly into yew makes it possible to implement a helper macro without the user pulling in an extra dependency and has tighter version requirements. --- Cargo.lock | 10 +++++----- examples/split-wasm/Cargo.toml | 1 - examples/split-wasm/build.sh | 3 +-- examples/split-wasm/src/yew.rs | 1 - packages/yew/Cargo.toml | 1 + packages/yew/src/lazy.rs | 4 +++- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a53fc1e65f..cadcb977c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3011,7 +3011,6 @@ dependencies = [ "async-once-cell", "wasm-bindgen", "wasm-bindgen-futures", - "wasm_split_helpers", "web-sys", "yew", ] @@ -3917,9 +3916,9 @@ dependencies = [ [[package]] name = "wasm_split_helpers" -version = "0.2.0-rc.1" +version = "0.2.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a63f86bb241fa35d6e87c50311e1abb9e403c5db690f3637525d6fa2876b8e" +checksum = "f68a5378142b01290e67fb6da5e8cc60eadccabab375bf57e1ac93c525d91053" dependencies = [ "async-once-cell", "wasm_split_macros", @@ -3927,9 +3926,9 @@ dependencies = [ [[package]] name = "wasm_split_macros" -version = "0.2.0-rc.1" +version = "0.2.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee3692283763ee89e2eb075c51fff63c6385d6a31bcb1931262dc44180e58fb" +checksum = "53f4b5cb3cc6173a614b7aef0573ee5ded1d0396f4ec634a9a8612e7bb027ca4" dependencies = [ "base16", "quote", @@ -4312,6 +4311,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "wasm_split_helpers", "web-sys", "yew-macro", ] diff --git a/examples/split-wasm/Cargo.toml b/examples/split-wasm/Cargo.toml index 61f8c168173..0e7ac495cc9 100644 --- a/examples/split-wasm/Cargo.toml +++ b/examples/split-wasm/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT OR Apache-2.0" [dependencies] async-once-cell = "0.5.3" yew = { path = "../../packages/yew", features = ["csr"] } -wasm_split_helpers = "0.2.0-rc.1" wasm-bindgen = "*" wasm-bindgen-futures = "*" diff --git a/examples/split-wasm/build.sh b/examples/split-wasm/build.sh index 78fb7536275..abf5a6928ac 100755 --- a/examples/split-wasm/build.sh +++ b/examples/split-wasm/build.sh @@ -21,8 +21,7 @@ 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/ \ + wasm_split_cli --verbose "$TARGET_DIR/wasm32-unknown-unknown/${PROFILE}/split-wasm.wasm" "$THIS_DIR"/dist/.stage/ \ > "$THIS_DIR"/dist/.stage/split.log ) echo "running wasm-bindgen" diff --git a/examples/split-wasm/src/yew.rs b/examples/split-wasm/src/yew.rs index 3750dcd0339..6f428c74f2c 100644 --- a/examples/split-wasm/src/yew.rs +++ b/examples/split-wasm/src/yew.rs @@ -1,6 +1,5 @@ use std::future::{pending, Future}; -use wasm_split_helpers::wasm_split as split; use web_sys::HtmlInputElement; use yew::lazy::declare_lazy_component; use yew::prelude::*; diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 7a204cbbddc..c5bacfb80e0 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -34,6 +34,7 @@ serde = { workspace = true, features = ["derive"] } tracing = "0.1.44" tokise = "0.2.0" rustversion.workspace = true +wasm_split_helpers = "0.2.0-rc.2" async-once-cell = "0.5.3" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index 4b01b80e5fe..13f3de8fc63 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -190,7 +190,7 @@ macro_rules! __declare_lazy_component { type Underlying = $comp; async fn fetch() -> ::yew::lazy::LazyVTable { - #[split($module)] + #[$crate::lazy::wasm_split::wasm_split($module, wasm_split_path = $crate::lazy::wasm_split)] fn split_fetch() -> ::yew::lazy::LazyVTable<$comp> { ::yew::lazy::LazyVTable::<$comp>::vtable() } @@ -227,5 +227,7 @@ macro_rules! __declare_lazy_component { } #[doc(hidden)] pub use ::async_once_cell::Lazy as LazyCell; +#[doc(hidden)] +pub use ::wasm_split_helpers as wasm_split; pub use crate::__declare_lazy_component as declare_lazy_component; From c9001dd55d5aafa1beb36fc8157527be9575c5c8 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 16 Oct 2025 15:27:51 +0200 Subject: [PATCH 05/10] fix: feature soundness --- packages/yew/src/lazy.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index 13f3de8fc63..823a30c034a 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -5,7 +5,7 @@ use std::future::Future; -use crate::html::{Scope, Scoped}; +use crate::html::Scope; use crate::suspense::Suspension; use crate::{BaseComponent, Context, HtmlResult}; @@ -104,7 +104,17 @@ impl BaseComponent for Lazy { type Properties = ::Properties; fn create(ctx: &Context) -> Self { - let inner_scope = Scope::new(Some(ctx.link().to_any())); + #[cfg(not(any(feature = "ssr", feature = "csr")))] + { + let _ = ctx; + todo!("Component shouldn't render without any rendering mode enabled"); + } + #[allow(unreachable_code)] + let inner_scope; + #[cfg(any(feature = "ssr", feature = "csr"))] + { + inner_scope = Scope::new(Some(ctx.link().clone().into())); + } let creation_ctx = ctx.narrow_scope(&inner_scope); let link = ctx.link().clone(); From 3a930f1bb58865de5dd32922c9e1fbbb6018e822 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 22 Oct 2025 11:22:02 +0200 Subject: [PATCH 06/10] bump to release version --- Cargo.lock | 8 ++++---- packages/yew/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cadcb977c33..72de516da99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3916,9 +3916,9 @@ dependencies = [ [[package]] name = "wasm_split_helpers" -version = "0.2.0-rc.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68a5378142b01290e67fb6da5e8cc60eadccabab375bf57e1ac93c525d91053" +checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84" dependencies = [ "async-once-cell", "wasm_split_macros", @@ -3926,9 +3926,9 @@ dependencies = [ [[package]] name = "wasm_split_macros" -version = "0.2.0-rc.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f4b5cb3cc6173a614b7aef0573ee5ded1d0396f4ec634a9a8612e7bb027ca4" +checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0" dependencies = [ "base16", "quote", diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index c5bacfb80e0..6ec4197879a 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -34,7 +34,7 @@ serde = { workspace = true, features = ["derive"] } tracing = "0.1.44" tokise = "0.2.0" rustversion.workspace = true -wasm_split_helpers = "0.2.0-rc.2" +wasm_split_helpers = "0.2.0" async-once-cell = "0.5.3" [target.'cfg(target_arch = "wasm32")'.dependencies] From 59c3148460eb4ba0881096d20466f0eb5d09ce99 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 12 Mar 2026 00:00:56 +0100 Subject: [PATCH 07/10] fix message passing of components we accept the penalty of running a lot of lifecycle hooks twice: once for the Lazy<_> component, once for the inner one, and just forward messages and other things, instead of hacking around in Scope. --- examples/split-wasm/.cargo/config.toml | 2 + examples/split-wasm/build.sh | 3 +- packages/yew/src/html/component/mod.rs | 26 +--- packages/yew/src/html/component/scope.rs | 12 +- packages/yew/src/lazy.rs | 179 +++++++++++------------ packages/yew/src/virtual_dom/vcomp.rs | 34 ++++- 6 files changed, 137 insertions(+), 119 deletions(-) create mode 100644 examples/split-wasm/.cargo/config.toml diff --git a/examples/split-wasm/.cargo/config.toml b/examples/split-wasm/.cargo/config.toml new file mode 100644 index 00000000000..b65cc8fb321 --- /dev/null +++ b/examples/split-wasm/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(target_arch = "wasm32")'] +rustflags=["-Clink-args=--emit-relocs"] diff --git a/examples/split-wasm/build.sh b/examples/split-wasm/build.sh index abf5a6928ac..4f7b6b3d399 100755 --- a/examples/split-wasm/build.sh +++ b/examples/split-wasm/build.sh @@ -12,8 +12,7 @@ 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 \ +$CARGO build --target wasm32-unknown-unknown \ $(case $PROFILE in "debug") ;; "release") echo "--release" ;; *) echo '--profile "${PROFILE}"' ;; esac) mkdir -p dist/ diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 97acab24fea..47d7f34bb06 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -53,6 +53,12 @@ impl Context { &self.props } + /// The component's props as an Rc + #[inline] + pub(crate) fn rc_props(&self) -> &Rc { + &self.props + } + #[cfg(feature = "hydration")] pub(crate) fn creation_mode(&self) -> RenderMode { self.creation_mode @@ -68,26 +74,6 @@ 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/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 88ec635377b..3ceceaa2d43 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -390,7 +390,7 @@ mod feat_csr_ssr { use std::sync::atomic::{AtomicUsize, Ordering}; use super::*; - use crate::html::component::lifecycle::UpdateRunner; + use crate::html::component::lifecycle::{RenderRunner, UpdateRunner}; use crate::scheduler::{self, Shared}; #[derive(Debug)] @@ -473,6 +473,16 @@ mod feat_csr_ssr { scheduler::start(); } + #[inline] + pub(crate) fn schedule_render(&self) { + scheduler::push_component_render( + self.id, + Box::new(RenderRunner { + state: self.state.clone(), + }), + ); + } + #[inline] pub(super) fn arch_send_message(&self, msg: T) where diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index 823a30c034a..2ca634a5647 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -3,26 +3,23 @@ // A simple wrapper is easy to implement. This module exists to support message passing and more // involved logic +use std::cell::RefCell; use std::future::Future; +use std::rc::Rc; use crate::html::Scope; +use crate::scheduler::Shared; use crate::suspense::Suspension; -use crate::{BaseComponent, Context, HtmlResult}; +use crate::virtual_dom::VComp; +use crate::{BaseComponent, Context}; -// 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, - //... + /// The way to create a component from properties and a way to reference it later. + /// It is important that we return a structure that already captures the vtable to + /// the component's functionality (Mountable), so that the linker doesn't see that + /// the main module references C's BaseComponent impl. + wrap_html: fn(Rc, Shared>>) -> VComp, } /// Component vtable for a component. /// @@ -34,7 +31,7 @@ pub struct LazyVTable { impl std::fmt::Debug for LazyVTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LazyVTable") - .field("vtable", &"...") + .field("vtable", &(self.imp as *const _)) .finish() } } @@ -50,16 +47,16 @@ impl LazyVTable { /// /// Return this from [`LazyComponent::fetch`] for your lazy component. pub fn vtable() -> LazyVTable { + fn wrap_html( + props: Rc, + scope_ref: Shared>>, + ) -> VComp { + VComp::new_with_ref(props, scope_ref) + } 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, + wrap_html: wrap_html::, } }, } @@ -75,118 +72,110 @@ pub trait LazyComponent: 'static { fn fetch() -> impl Future> + Send; } -#[derive(Debug)] enum LazyState { Pending(Suspension), - Created(LazyVTable, C), + Created(LazyVTable), +} + +impl std::fmt::Debug for LazyState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pending(arg0) => f.debug_tuple("Pending").field(arg0).finish(), + Self::Created(arg0) => f.debug_tuple("Created").field(arg0).finish(), + } + } } + /// Wrapper for a lazily fetched component /// /// This component suspends as long as the underlying component is still being fetched, /// then behaves as the underlying component itself. -#[derive(Debug)] pub struct Lazy { - inner_scope: Scope, - state: LazyState, + inner_scope: Shared>>, + // messages sent to the component before the inner_scope is set are buffered + message_buffer: RefCell::Message>>, + state: Shared>, } -/// Message to send to a lazy component -#[derive(Debug)] -pub enum LazyMessage { - /// Forward the message to the underlying component once that is fetched - Forward(C::Message), - #[doc(hidden)] - FetchFinished(LazyVTable, C), +impl std::fmt::Debug for Lazy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Lazy") + .field("inner_scope", &self.inner_scope) + .field("message_buffer", &"...") + .field("state", &self.state) + .finish() + } } impl BaseComponent for Lazy { - type Message = LazyMessage; + type Message = ::Message; type Properties = ::Properties; fn create(ctx: &Context) -> Self { - #[cfg(not(any(feature = "ssr", feature = "csr")))] - { - let _ = ctx; - todo!("Component shouldn't render without any rendering mode enabled"); - } - #[allow(unreachable_code)] - let inner_scope; - #[cfg(any(feature = "ssr", feature = "csr"))] - { - inner_scope = Scope::new(Some(ctx.link().clone().into())); - } - let creation_ctx = ctx.narrow_scope(&inner_scope); - - let link = ctx.link().clone(); - let suspension = Suspension::from_future(async move { - // Ignore error in case receiver was dropped - let vtable = C::fetch().await; - let comp = (vtable.imp.create)(&creation_ctx); - link.send_message(LazyMessage::FetchFinished(vtable, comp)); + let host_scope = ctx.link().clone(); + let state = Rc::>::new_cyclic(move |state| { + let state = state.clone(); + let suspension = Suspension::from_future(async move { + // Ignore error in case receiver was dropped + let vtable = C::fetch().await; + #[cfg(any(feature = "ssr", feature = "csr"))] + if let Some(state) = state.upgrade() { + *state.borrow_mut() = LazyState::Created(vtable); + // force a re-render with this new state (without a message exchange) + host_scope.schedule_render(); + } + }); + RefCell::new(LazyState::Pending(suspension)) }); Self { - inner_scope, - state: LazyState::Pending(suspension), + inner_scope: Rc::default(), + message_buffer: RefCell::default(), + state, } } - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let msg = match msg { - LazyMessage::FetchFinished(vtable, comp) => { - self.state = LazyState::Created(vtable, comp); - return true; - } - LazyMessage::Forward(msg) => msg, - }; - match &mut self.state { - LazyState::Pending(_) => { - // has a queueing implementation. We don't rerender until the suspension resolves - self.inner_scope.send_message(msg); - false - } - LazyState::Created(vtable, comp) => { - (vtable.imp.update)(comp, &ctx.narrow_scope(&self.inner_scope), msg) - } + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + if let Some(inner) = self.inner_scope.borrow().as_ref() { + inner.send_message(msg); + } else { + self.message_buffer.borrow_mut().push(msg); } + false } - fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { - if let LazyState::Created(vtable, comp) = &mut self.state { - (vtable.imp.changed)(comp, &ctx.narrow_scope(&self.inner_scope), old_props) - } else { - false - } + fn changed(&mut self, _: &Context, _old_props: &Self::Properties) -> bool { + true } fn view(&self, ctx: &Context) -> crate::HtmlResult { - match &self.state { + match &*self.state.borrow() { LazyState::Pending(suspension) => Err(suspension.clone().into()), - LazyState::Created(vtable, comp) => { - (vtable.imp.view)(comp, &ctx.narrow_scope(&self.inner_scope)) + LazyState::Created(lazy_vtable) => { + let comp = + (lazy_vtable.imp.wrap_html)(ctx.rc_props().clone(), self.inner_scope.clone()); + Ok(comp.into()) } } } - fn rendered(&mut self, ctx: &Context, first_render: bool) { - if let LazyState::Created(vtable, comp) = &mut self.state { - (vtable.imp.rendered)(comp, &ctx.narrow_scope(&self.inner_scope), first_render) + fn rendered(&mut self, _: &Context, first_render: bool) { + if first_render { + let inner = self.inner_scope.borrow(); + let inner = inner.as_ref().expect("lazy component to have rendered"); + inner.send_message_batch(std::mem::take(&mut *self.message_buffer.borrow_mut())); } else { - unreachable!("can't get rendered before fetching the vtable") + #[cfg(debug_assertions)] + assert!( + self.message_buffer.borrow().is_empty(), + "no message in buffer after first render" + ); } } - fn destroy(&mut self, ctx: &Context) { - if let LazyState::Created(vtable, comp) = &mut self.state { - (vtable.imp.destroy)(comp, &ctx.narrow_scope(&self.inner_scope)) - } - } + fn destroy(&mut self, _: &Context) {} fn prepare_state(&self) -> Option { - if let LazyState::Created(vtable, comp) = &self.state { - (vtable.imp.prepare_state)(comp) - } else { - None - } + None } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 0dbc9ce7443..9915aae7008 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -19,6 +19,7 @@ use crate::html::BaseComponent; use crate::html::Scoped; #[cfg(any(feature = "ssr", feature = "csr"))] use crate::html::{AnyScope, Scope}; +use crate::scheduler::Shared; #[cfg(feature = "ssr")] use crate::{feat_ssr::VTagKind, platform::fmt::BufWriter}; @@ -92,11 +93,25 @@ pub(crate) trait Mountable { pub(crate) struct PropsWrapper { props: Rc, + scope_ref: Option>>>, } impl PropsWrapper { pub fn new(props: Rc) -> Self { - Self { props } + Self { + props, + scope_ref: None, + } + } + + pub fn new_with_ref( + props: Rc, + scope_ref: Shared>>, + ) -> Self { + Self { + props, + scope_ref: Some(scope_ref), + } } } @@ -104,6 +119,7 @@ impl Mountable for PropsWrapper { fn copy(&self) -> Box { let wrapper: PropsWrapper = PropsWrapper { props: Rc::clone(&self.props), + scope_ref: self.scope_ref.clone(), }; Box::new(wrapper) } @@ -128,6 +144,9 @@ impl Mountable for PropsWrapper { slot: DomSlot, ) -> (Box, DynamicDomSlot) { let scope: Scope = Scope::new(Some(parent_scope.clone())); + if let Some(scope_ref) = self.scope_ref { + *scope_ref.borrow_mut() = Some(scope.clone()); + } let own_slot = scope.mount_in_place(root.clone(), parent, slot, self.props); (Box::new(scope), own_slot) @@ -248,6 +267,19 @@ impl VComp { _marker: 0, } } + + /// Attach a ref into which the scope of the child will be written when it is mounted + pub(crate) fn new_with_ref( + props: Rc, + scope_ref: Shared>>, + ) -> Self { + VComp { + type_id: TypeId::of::(), + mountable: Box::new(PropsWrapper::::new_with_ref(props, scope_ref)), + key: None, + _marker: 0, + } + } } impl PartialEq for VComp { From a3debc962e49a4f21120cce830f7ddb9022abafb Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 12 Mar 2026 00:08:18 +0100 Subject: [PATCH 08/10] fix: feature soundness --- packages/yew/src/lazy.rs | 2 ++ packages/yew/src/virtual_dom/vcomp.rs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index 2ca634a5647..a7267d510c2 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -74,6 +74,7 @@ pub trait LazyComponent: 'static { enum LazyState { Pending(Suspension), + #[allow(unused)] // Only constructed with feature csr or ssr Created(LazyVTable), } @@ -124,6 +125,7 @@ impl BaseComponent for Lazy { // force a re-render with this new state (without a message exchange) host_scope.schedule_render(); } + let _ = (host_scope, state, vtable); }); RefCell::new(LazyState::Pending(suspension)) }); diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 9915aae7008..dba93b5301a 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -14,11 +14,11 @@ use super::Key; use crate::dom_bundle::Fragment; #[cfg(feature = "csr")] use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot}; -use crate::html::BaseComponent; +#[cfg(any(feature = "ssr", feature = "csr"))] +use crate::html::AnyScope; #[cfg(feature = "csr")] use crate::html::Scoped; -#[cfg(any(feature = "ssr", feature = "csr"))] -use crate::html::{AnyScope, Scope}; +use crate::html::{BaseComponent, Scope}; use crate::scheduler::Shared; #[cfg(feature = "ssr")] use crate::{feat_ssr::VTagKind, platform::fmt::BufWriter}; From 5b1ef2edae7c61e6696e20f96cfe949e1ed9efee Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 12 Mar 2026 00:35:57 +0100 Subject: [PATCH 09/10] fix some lints and use $crate consistently in macro_rules --- packages/yew/src/lazy.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/yew/src/lazy.rs b/packages/yew/src/lazy.rs index a7267d510c2..05b62488f84 100644 --- a/packages/yew/src/lazy.rs +++ b/packages/yew/src/lazy.rs @@ -13,13 +13,15 @@ use crate::suspense::Suspension; use crate::virtual_dom::VComp; use crate::{BaseComponent, Context}; +type ScopeRef = Shared>>; + #[derive(Debug)] struct CompVTableImpl { /// The way to create a component from properties and a way to reference it later. /// It is important that we return a structure that already captures the vtable to /// the component's functionality (Mountable), so that the linker doesn't see that /// the main module references C's BaseComponent impl. - wrap_html: fn(Rc, Shared>>) -> VComp, + wrap_html: fn(Rc, ScopeRef) -> VComp, } /// Component vtable for a component. /// @@ -92,7 +94,7 @@ impl std::fmt::Debug for LazyState { /// This component suspends as long as the underlying component is still being fetched, /// then behaves as the underlying component itself. pub struct Lazy { - inner_scope: Shared>>, + inner_scope: ScopeRef, // messages sent to the component before the inner_scope is set are buffered message_buffer: RefCell::Message>>, state: Shared>, @@ -187,26 +189,26 @@ impl BaseComponent for Lazy { macro_rules! __declare_lazy_component { ($comp:ty as $lazy_name:ident in $module:ident) => { struct Proxy; - impl ::yew::lazy::LazyComponent for Proxy { + impl $crate::lazy::LazyComponent for Proxy { type Underlying = $comp; - async fn fetch() -> ::yew::lazy::LazyVTable { + async fn fetch() -> $crate::lazy::LazyVTable { #[$crate::lazy::wasm_split::wasm_split($module, wasm_split_path = $crate::lazy::wasm_split)] - fn split_fetch() -> ::yew::lazy::LazyVTable<$comp> { - ::yew::lazy::LazyVTable::<$comp>::vtable() + fn split_fetch() -> $crate::lazy::LazyVTable<$comp> { + $crate::lazy::LazyVTable::<$comp>::vtable() } struct F( ::std::option::Option< ::std::pin::Pin< ::std::boxed::Box< - dyn ::std::future::Future> + dyn ::std::future::Future> + ::std::marker::Send, >, >, >, ); impl Future for F { - type Output = ::yew::lazy::LazyVTable<$comp>; + type Output = $crate::lazy::LazyVTable<$comp>; fn poll( mut self: ::std::pin::Pin<&mut Self>, @@ -218,12 +220,12 @@ macro_rules! __declare_lazy_component { .poll(cx) } } - static CACHE: ::yew::lazy::LazyCell<::yew::lazy::LazyVTable<$comp>, F> = - ::yew::lazy::LazyCell::new(F(None)); + static CACHE: $crate::lazy::LazyCell<$crate::lazy::LazyVTable<$comp>, F> = + $crate::lazy::LazyCell::new(F(None)); *::std::pin::Pin::static_ref(&CACHE).await.get_ref() } } - type $lazy_name = ::yew::lazy::Lazy; + type $lazy_name = $crate::lazy::Lazy; }; } #[doc(hidden)] From 26921f80e8054f5ec0216eb35c1bbe5d44300146 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 17 Mar 2026 16:19:34 +0100 Subject: [PATCH 10/10] fix unused lint for start_now --- packages/yew/src/scheduler.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 0a710b95419..1081fb26895 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -211,6 +211,13 @@ mod feat_hydration { pub(crate) use feat_hydration::*; /// Execute any pending [Runnable]s +#[cfg(any( + test, + feature = "test", + not(target_arch = "wasm32"), + target_os = "wasi", + feature = "not_browser_env" +))] pub(crate) fn start_now() { #[tracing::instrument(level = tracing::Level::DEBUG)] fn scheduler_loop() {