feat: router-based wasm bundle splitting (#3683)
* feat: wasm-split base implementation * kinda sorta works with the cli * okay great it actually works * fix: cache busting for chunks * cleaned up version * it works with the router * wip: migrate to a faster linear solution using id remapping * add a good harness * it works completely on the docsite * the super tight build works too * make parallelizable * works on the docsite but not the harness * light cleanups, fix for both harness and docsite * make codegen smaller by extract some shared functions to prevent monos * chunking works, but tbd on tuning heuristics * extract used module * wip: cleaning up module * wip: much better understanding of how imports work * fix the call graph * working again... * wip: thinking about ifuncs * ifunc approach works * yes the ifunc approach works!! * it all works completely even with chunks * small cleanups * comments * actually locate all of the symbols * clean up final implementation * clean up tomls, revert some mono changs * clean up, add some more comments / docs in places * hoist more packages * clean up impl a bit more * clean cli * move harness to playwright * update playwright * extract codgen * add lazy to components * Add docs to split loader * clippy * typos * clippy, don't generate splits if not on wasm * whoops, const * Typos and docs * enable optimizations in ci * add optimizations to ci * add wasm-split router feature * parallel playwright? * fix no case check * disable pre-compress... think that might be the issue * better logging, no default pre-compress, caching of shared symbols * Fix: compressing assets * fix non-interactive logging for serve * cache playwright even on failure, use the cli with optimizations * fix prebuild * fix playwright webserver
This commit is contained in:
1 parent
14eb7597df
commit
bdeedc13eb
59 files changed
+4249
-299
No files matched your search
@@ -19,13 +19,16 @@ syn = { workspace = true, features = ["extra-traits", "full"] }
|
||||
quote = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
slab = { workspace = true }
|
||||
base16 = { workspace = true }
|
||||
digest = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = []
|
||||
web = [] #todo(jon) remove this before releasing 0.7!
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
@@ -475,82 +475,9 @@ impl RouteEnum {
|
||||
site_map,
|
||||
};
|
||||
|
||||
// If we're on the web, only the URL history is preserved between navigation. We need to warn the user that the segment is not present in the URL.
|
||||
if cfg!(feature = "web") {
|
||||
for variant in &data.variants {
|
||||
for field in &variant.fields {
|
||||
if !myself.field_present_in_url(field.ident.as_ref().unwrap()) {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field.ident.as_ref().unwrap(),
|
||||
format!("The `{}` field must be present in the url for the web history. You can include the field in the url by using the `#[route(\"/:{}\")]` attribute on the enum variant.", field.ident.as_ref().unwrap(), field.ident.as_ref().unwrap()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(myself)
|
||||
}
|
||||
|
||||
fn field_present_in_url(&self, field: &Ident) -> bool {
|
||||
let mut from_route = false;
|
||||
|
||||
for nest in &self.nests {
|
||||
if nest.dynamic_segments_names().any(|i| &i == field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
for route in &self.endpoints {
|
||||
match route {
|
||||
RouteEndpoint::Route(route) => match &route.ty {
|
||||
RouteType::Child(child) => {
|
||||
if let Some(child) = child.ident.as_ref() {
|
||||
if child == "child" {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
}
|
||||
RouteType::Leaf { .. } => {
|
||||
for segment in &route.segments {
|
||||
if segment.name().as_ref() == Some(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
if let Some(query) = &route.query {
|
||||
if query.contains_ident(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
if let Some(hash) = &route.hash {
|
||||
if hash.contains_ident(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
RouteEndpoint::Redirect(redirect) => {
|
||||
for segment in &redirect.segments {
|
||||
if segment.name().as_ref() == Some(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
if let Some(query) = &redirect.query {
|
||||
if query.contains_ident(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
if let Some(hash) = &redirect.hash {
|
||||
if hash.contains_ident(field) {
|
||||
from_route = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from_route
|
||||
}
|
||||
|
||||
fn impl_display(&self) -> TokenStream2 {
|
||||
let mut display_match = Vec::new();
|
||||
|
||||
@@ -737,7 +664,7 @@ impl RouteEnum {
|
||||
// Collect all routes matches
|
||||
for route in &self.endpoints {
|
||||
if let RouteEndpoint::Route(route) = route {
|
||||
matches.push(route.routable_match(&self.layouts, &self.nests));
|
||||
matches.push(route.routable_match(&self.layouts, &self.nests, name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ pub(crate) struct Route {
|
||||
}
|
||||
|
||||
impl Route {
|
||||
pub fn parse(
|
||||
pub(crate) fn parse(
|
||||
nests: Vec<NestId>,
|
||||
layouts: Vec<LayoutId>,
|
||||
variant: syn::Variant,
|
||||
@@ -167,7 +167,7 @@ impl Route {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
|
||||
pub(crate) fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
|
||||
let name = &self.route_name;
|
||||
let dynamic_segments = self.dynamic_segments();
|
||||
let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
|
||||
@@ -211,7 +211,12 @@ impl Route {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routable_match(&self, layouts: &[Layout], nests: &[Nest]) -> TokenStream2 {
|
||||
pub(crate) fn routable_match(
|
||||
&self,
|
||||
layouts: &[Layout],
|
||||
nests: &[Nest],
|
||||
router_name: &Ident,
|
||||
) -> TokenStream2 {
|
||||
let name = &self.route_name;
|
||||
|
||||
let mut tokens = TokenStream2::new();
|
||||
@@ -265,12 +270,91 @@ impl Route {
|
||||
RouteType::Leaf { component } => {
|
||||
let dynamic_segments = self.dynamic_segments();
|
||||
let dynamic_segments_from_route = self.dynamic_segments();
|
||||
|
||||
/*
|
||||
The implementation of this is pretty gnarly/gross.
|
||||
|
||||
We achieve the bundle splitting by wrapping the incoming function in a new component
|
||||
that suspends based on an internal lazy loader. This lets us use suspense features
|
||||
without breaking the rules of hooks. The router derive is quite complex so this shoves
|
||||
the complexity towards the "leaf" of the codegen rather to its core. In the future though,
|
||||
we should think about restructuring the router macro completely since its codegen
|
||||
makes up nearly 30-40% of the binary size in the dioxus docsite.
|
||||
*/
|
||||
use sha2::Digest;
|
||||
let dynamic_segments_receiver = self.dynamic_segments();
|
||||
let dynamic_segments_from_route_ = self.dynamic_segments();
|
||||
let dynamic_segments_from_route__ = self.dynamic_segments();
|
||||
let unique_identifier = base16::encode_lower(
|
||||
&sha2::Sha256::digest(format!("{name} {span:?}", span = name.span()))[..16],
|
||||
);
|
||||
let module_name = format_ident!("module{}{unique_identifier}", name).to_string();
|
||||
let comp_name = format_ident!("route{}{unique_identifier}", name);
|
||||
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
(#last_index, Self::#name { #(#dynamic_segments,)* }) => {
|
||||
rsx! {
|
||||
#component {
|
||||
#(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
|
||||
dioxus::config_macros::maybe_wasm_split! {
|
||||
if wasm_split {
|
||||
{
|
||||
fn #comp_name(args: #router_name) -> Element {
|
||||
match args {
|
||||
#router_name::#name { #(#dynamic_segments_from_route_,)* } => {
|
||||
rsx! {
|
||||
#component {
|
||||
#(#dynamic_segments_from_route__: #dynamic_segments_from_route__,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[component]
|
||||
fn LoaderInner(args: NoPartialEq<#router_name>) -> Element {
|
||||
static MODULE: wasm_split::LazyLoader<#router_name, Element> =
|
||||
wasm_split::lazy_loader!(extern #module_name fn #comp_name(props: #router_name) -> Element);
|
||||
|
||||
use_resource(|| async move { MODULE.load().await }).suspend()?;
|
||||
MODULE.call(args.0).unwrap()
|
||||
}
|
||||
|
||||
struct NoPartialEq<T>(T);
|
||||
|
||||
impl<T: Clone> Clone for NoPartialEq<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display> std::fmt::Display for NoPartialEq<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for NoPartialEq<T> {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
LoaderInner {
|
||||
args: NoPartialEq(#router_name::#name { #(#dynamic_segments_receiver,)* } )
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{
|
||||
rsx! {
|
||||
#component {
|
||||
#(#dynamic_segments_from_route: #dynamic_segments_from_route,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +371,7 @@ impl Route {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
|
||||
pub(crate) fn construct(&self, nests: &[Nest], enum_name: Ident) -> TokenStream2 {
|
||||
let segments = self.fields.iter().map(|(name, _)| {
|
||||
let mut from_route = false;
|
||||
|
||||
@@ -343,11 +427,11 @@ impl Route {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_ident(&self) -> Ident {
|
||||
pub(crate) fn error_ident(&self) -> Ident {
|
||||
format_ident!("{}ParseError", self.route_name)
|
||||
}
|
||||
|
||||
pub fn error_type(&self) -> TokenStream2 {
|
||||
pub(crate) fn error_type(&self) -> TokenStream2 {
|
||||
let error_name = self.error_ident();
|
||||
let child_type = match &self.ty {
|
||||
RouteType::Child(field) => Some(&field.ty),
|
||||
@@ -357,14 +441,14 @@ impl Route {
|
||||
create_error_type(&self.route, error_name, &self.segments, child_type)
|
||||
}
|
||||
|
||||
pub fn parse_query(&self) -> TokenStream2 {
|
||||
pub(crate) fn parse_query(&self) -> TokenStream2 {
|
||||
match &self.query {
|
||||
Some(query) => query.parse(),
|
||||
None => quote! {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_hash(&self) -> TokenStream2 {
|
||||
pub(crate) fn parse_hash(&self) -> TokenStream2 {
|
||||
match &self.hash {
|
||||
Some(hash) => hash.parse(),
|
||||
None => quote! {},
|
||||
|
||||
Reference in new issue
Block a user