perf: further reduce WASM binary size by ~5-7% (#459)

* Update `leptos_router` docs
* Further reducing WASM bundle sizes
This commit is contained in:
Greg Johnston 2023-02-03 17:38:44 -05:00 committed by GitHub
parent c4e693e01e
commit 6d0d70cd17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 349 additions and 299 deletions

View file

@ -154,167 +154,182 @@ where
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
let Self { id, child_fn } = self;
// concrete inner function
fn create_dyn_view(
cx: Scope,
component: DynChildRepr,
child_fn: Box<dyn Fn() -> View>,
) -> DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
let component = DynChildRepr::new_with_id(id);
let child = component.child.clone();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
let span = tracing::Span::current();
let child = component.child.clone();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
let span = tracing::Span::current();
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
let mut child_borrow = child.borrow_mut();
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
let mut child_borrow = child.borrow_mut();
// Dispose of the scope
prev_disposer.dispose();
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// Dispose of the scope
prev_disposer.dispose();
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
**child_borrow = Some(new_child);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
(Some(prev_t), disposer)
} else {
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child.clone());
(Some(new_t.node.clone()), disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child);
(Some(prev_t), disposer)
} else {
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child.clone());
(Some(new_t.node.clone()), disposer)
(None, disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
}
// Mount the new child
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to reuse text nodes, so hold onto it if
// our child is one
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
t.remove();
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child);
(None, disposer)
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
}
// Mount the new child
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to reuse text nodes, so hold onto it if
// our child is one
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
t.remove();
mount_child(MountKind::Before(&closing), &new_child);
}
},
);
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(MountKind::Before(&closing), &new_child);
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child.borrow_mut() = Some(new_child);
}
**child_borrow = Some(new_child);
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
component
}
// monomorphized outer function
let Self { id, child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}

View file

@ -1 +0,0 @@

View file

@ -16,7 +16,8 @@ thread_local! {
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: Cow<'static, str>,
mut cb: impl FnMut(E) + 'static,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{

View file

@ -200,7 +200,7 @@ where
}
impl EffectId {
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
pub(crate) fn run(&self, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();

View file

@ -130,7 +130,8 @@ where
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
let r = Rc::clone(&r) as Rc<dyn SerializableResource>;
runtime.create_serializable_resource(r)
})
.expect("tried to create a Resource in a Runtime that has been disposed.");
@ -250,7 +251,8 @@ where
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
let r = Rc::clone(&r) as Rc<dyn UnserializableResource>;
runtime.create_unserializable_resource(r)
})
.expect("tried to create a Resource in a runtime that has been disposed.");

View file

@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
hydration::SharedContext, serialization::Serializable, AnyEffect, AnyResource, Effect,
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SignalId, WriteSignal,
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
SerializableResource, SignalId, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
@ -115,18 +115,19 @@ impl RuntimeId {
ret
}
#[track_caller]
pub(crate) fn create_concrete_signal(self, value: Rc<RefCell<dyn Any>>) -> SignalId {
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
.expect("tried to create a signal in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
(
ReadSignal {
runtime: self,
@ -149,13 +150,7 @@ impl RuntimeId {
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
RwSignal {
runtime: self,
id,
@ -165,6 +160,12 @@ impl RuntimeId {
}
}
#[track_caller]
pub(crate) fn create_concrete_effect(self, effect: Rc<dyn AnyEffect>) -> EffectId {
with_runtime(self, |runtime| runtime.effects.borrow_mut().insert(effect))
.expect("tried to create an effect in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
@ -173,18 +174,16 @@ impl RuntimeId {
#[cfg(debug_assertions)]
let defined_at = std::panic::Location::caller();
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
})
.expect("tried to create an effect in a runtime that has been disposed")
let effect = Effect {
f,
value: RefCell::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = self.create_concrete_effect(Rc::new(effect));
id.run(self);
id
}
#[track_caller]
@ -256,27 +255,19 @@ impl Runtime {
Self::default()
}
pub(crate) fn create_unserializable_resource<S, T>(
pub(crate) fn create_unserializable_resource(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: 'static,
{
state: Rc<dyn UnserializableResource>,
) -> ResourceId {
self.resources
.borrow_mut()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
pub(crate) fn create_serializable_resource(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: Serializable + 'static,
{
state: Rc<dyn SerializableResource>,
) -> ResourceId {
self.resources
.borrow_mut()
.insert(AnyResource::Serializable(state))

View file

@ -43,86 +43,109 @@ pub fn Form<A>(
where
A: ToHref + 'static,
{
let action_version = version;
let action = use_resolved_path(cx, move || action.to_href()());
fn inner(
cx: Scope,
method: Option<&'static str>,
action: Memo<Option<String>>,
enctype: Option<String>,
version: Option<RwSignal<usize>>,
error: Option<RwSignal<Option<Box<dyn Error>>>>,
#[allow(clippy::type_complexity)] on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
#[allow(clippy::type_complexity)] on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
children: Children,
) -> HtmlElement<Form> {
let action_version = version;
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
let (form, method, action, enctype) = extract_form_attributes(&ev);
let (form, method, action, enctype) = extract_form_attributes(&ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// POST
if method == "post" {
ev.prevent_default();
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// POST
if method == "post" {
ev.prevent_default();
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
}
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default());
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default());
}
}
}
}
}
});
}
// otherwise, GET
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
ev.prevent_default();
});
}
// otherwise, GET
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
ev.prevent_default();
}
}
};
let method = method.unwrap_or("get");
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children(cx)}
</form>
}
};
let method = method.unwrap_or("get");
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children(cx)}
</form>
}
let action = use_resolved_path(cx, move || action.to_href()());
inner(
cx,
method,
action,
enctype,
version,
error,
on_form_data,
on_response,
children,
)
}
/// Automatically turns a server [Action](leptos_server::Action) into an HTML

View file

@ -70,35 +70,47 @@ pub fn A<H>(
where
H: ToHref + 'static,
{
let location = use_location(cx);
let href = use_resolved_path(cx, move || href.to_href()());
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
fn inner(
cx: Scope,
href: Memo<Option<String>>,
exact: bool,
state: Option<State>,
replace: bool,
class: Option<MaybeSignal<String>>,
children: Children,
) -> HtmlElement<A> {
let location = use_location(cx);
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
Some(to) => {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
let loc = location.pathname.get().to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
Some(to) => {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
let loc = location.pathname.get().to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
}
}
}
});
});
view! { cx,
<a
href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
view! { cx,
<a
href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
}
}
let href = use_resolved_path(cx, move || href.to_href()());
inner(cx, href, exact, state, replace, class, children)
}

View file

@ -36,31 +36,47 @@ where
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display,
{
let children = children
.map(|children| {
children(cx)
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
id,
path: path.to_string(),
children,
view: Rc::new(move |cx| view(cx).into_view(cx)),
fn inner(
cx: Scope,
children: Option<Children>,
path: String,
view: Rc<dyn Fn(Scope) -> View>,
) -> RouteDefinition {
let children = children
.map(|children| {
children(cx)
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
id,
path,
children,
view,
}
}
inner(
cx,
children,
path.to_string(),
Rc::new(move |cx| view(cx).into_view(cx)),
)
}
impl IntoView for RouteDefinition {

View file

@ -8,10 +8,6 @@
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! **Note:** This is a work in progress. The feature to pass client-side route [State] in
//! [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state), in particular,
//! is incomplete.
//!
//! ## Philosophy
//!
//! Leptos Router is built on a few simple principles:
@ -23,12 +19,7 @@
//! and are rendered by different components. This means you can navigate between siblings
//! in this tree without re-rendering or triggering any change in the parent routes.
//!
//! 3. **Route-based data loading.** Each route should know exactly which data it needs
//! to render itself when the route is defined. This allows each routes data to be
//! reloaded independently, and allows data from nested routes to be loaded in parallel,
//! avoiding waterfalls.
//!
//! 4. **Progressive enhancement.** The [A] and [Form] components resolve any relative
//! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using
//! them with server-side rendering (with or without hydration), they just work,