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:
Jonathan Kelley authored and GitHub committed 2025-02-13 18:54:38 -08:00
1 parent 14eb7597df
commit bdeedc13eb
59 files changed
+4249 -299

No files matched your search

+4 -1
View File
@@ -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"]
+1 -74
View File
@@ -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));
}
}
+95 -11
View File
@@ -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! {},