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

View file

@ -1 +0,0 @@

View file

@ -16,7 +16,8 @@ thread_local! {
pub fn add_event_listener<E>( pub fn add_event_listener<E>(
target: &web_sys::Element, target: &web_sys::Element,
event_name: Cow<'static, str>, 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 ) where
E: FromWasmAbi + 'static, E: FromWasmAbi + 'static,
{ {

View file

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

View file

@ -130,7 +130,8 @@ where
}); });
let id = with_runtime(cx.runtime, |runtime| { 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."); .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| { 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."); .expect("tried to create a Resource in a runtime that has been disposed.");

View file

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

View file

@ -43,86 +43,109 @@ pub fn Form<A>(
where where
A: ToHref + 'static, A: ToHref + 'static,
{ {
let action_version = version; fn inner(
let action = use_resolved_path(cx, move || action.to_href()()); 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| { let (form, method, action, enctype) = extract_form_attributes(&ev);
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
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(); let on_response = on_response.clone();
if let Some(on_form_data) = on_form_data.clone() { spawn_local(async move {
on_form_data(&form_data); let res = gloo_net::http::Request::post(&action)
} .header("Accept", "application/json")
let params = .header("Content-Type", &enctype)
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw(); .body(params)
let action = use_resolved_path(cx, move || action.clone()) .send()
.get() .await;
.unwrap_or_default(); match res {
// POST Err(e) => {
if method == "post" { log::error!("<Form/> error while POSTing: {e:#?}");
ev.prevent_default(); if let Some(error) = error {
error.set(Some(Box::new(e)));
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());
} }
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 resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") { if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default()); _ = 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 /// Automatically turns a server [Action](leptos_server::Action) into an HTML

View file

@ -70,35 +70,47 @@ pub fn A<H>(
where where
H: ToHref + 'static, H: ToHref + 'static,
{ {
let location = use_location(cx); fn inner(
let href = use_resolved_path(cx, move || href.to_href()()); cx: Scope,
let is_active = create_memo(cx, move |_| match href.get() { href: Memo<Option<String>>,
None => false, 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) => { Some(to) => {
let path = to let path = to
.split(['?', '#']) .split(['?', '#'])
.next() .next()
.unwrap_or_default() .unwrap_or_default()
.to_lowercase(); .to_lowercase();
let loc = location.pathname.get().to_lowercase(); let loc = location.pathname.get().to_lowercase();
if exact { if exact {
loc == path loc == path
} else { } else {
loc.starts_with(&path) loc.starts_with(&path)
}
} }
} });
});
view! { cx, view! { cx,
<a <a
href=move || href.get().unwrap_or_default() href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())} prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace} prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None } aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get()) class=move || class.as_ref().map(|class| class.get())
> >
{children(cx)} {children(cx)}
</a> </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, F: Fn(Scope) -> E + 'static,
P: std::fmt::Display, P: std::fmt::Display,
{ {
let children = children fn inner(
.map(|children| { cx: Scope,
children(cx) children: Option<Children>,
.as_children() path: String,
.iter() view: Rc<dyn Fn(Scope) -> View>,
.filter_map(|child| { ) -> RouteDefinition {
child let children = children
.as_transparent() .map(|children| {
.and_then(|t| t.downcast_ref::<RouteDefinition>()) children(cx)
}) .as_children()
.cloned() .iter()
.collect::<Vec<_>>() .filter_map(|child| {
}) child
.unwrap_or_default(); .as_transparent()
let id = ROUTE_ID.with(|id| { .and_then(|t| t.downcast_ref::<RouteDefinition>())
let next = id.get() + 1; })
id.set(next); .cloned()
next .collect::<Vec<_>>()
}); })
RouteDefinition { .unwrap_or_default();
id,
path: path.to_string(), let id = ROUTE_ID.with(|id| {
children, let next = id.get() + 1;
view: Rc::new(move |cx| view(cx).into_view(cx)), 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 { impl IntoView for RouteDefinition {

View file

@ -8,10 +8,6 @@
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize //! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two. //! 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 //! ## Philosophy
//! //!
//! Leptos Router is built on a few simple principles: //! 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 //! 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. //! 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 //! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
//! 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
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible) //! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using //! 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, //! them with server-side rendering (with or without hydration), they just work,