Explorar el Código

Expose streaming options to choose between full prerendering and out of order streaming (#3288)

* expose streaming options to choose between full prerendering and out of order streaming

* add examples to streaming_mode
Evan Almloff hace 6 meses
padre
commit
29a7325bb7

+ 5 - 1
example-projects/fullstack-hackernews/src/main.rs

@@ -17,7 +17,11 @@ fn main() {
     #[cfg(feature = "server")]
     tracing_subscriber::fmt::init();
 
-    dioxus::launch(|| rsx! { Router::<Route> {} });
+    LaunchBuilder::new()
+        .with_cfg(server_only! {
+            dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
+        })
+        .launch(|| rsx! { Router::<Route> {} });
 }
 
 #[derive(Clone, Routable)]

+ 82 - 61
packages/fullstack/src/render.rs

@@ -3,18 +3,19 @@ use crate::document::ServerDocument;
 use crate::streaming::{Mount, StreamingRenderer};
 use dioxus_cli_config::base_path;
 use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
-use dioxus_isrg::{CachedRender, RenderFreshness};
+use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
 use dioxus_lib::document::Document;
 use dioxus_ssr::Renderer;
 use futures_channel::mpsc::Sender;
 use futures_util::{Stream, StreamExt};
+use std::fmt::Write;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::RwLock;
 use std::{collections::HashMap, future::Future};
 use tokio::task::JoinHandle;
 
-use crate::prelude::*;
+use crate::{prelude::*, StreamingMode};
 use dioxus_lib::prelude::*;
 
 /// A suspense boundary that is pending with a placeholder in the client
@@ -165,6 +166,7 @@ impl SsrRendererPool {
             .unwrap_or_else(pre_renderer);
 
         let myself = self.clone();
+        let streaming_mode = cfg.streaming_mode;
 
         let join_handle = spawn_platform(move || async move {
             let mut virtual_dom = virtual_dom_factory();
@@ -202,65 +204,10 @@ impl SsrRendererPool {
             {
                 let scope_to_mount_mapping = scope_to_mount_mapping.clone();
                 let stream = stream.clone();
-                // We use a stack to keep track of what suspense boundaries we are nested in to add children to the correct boundary
-                // The stack starts with the root scope because the root is a suspense boundary
-                let pending_suspense_boundaries_stack = RwLock::new(vec![]);
-                renderer.set_render_components(move |renderer, to, vdom, scope| {
-                    let is_suspense_boundary =
-                        SuspenseContext::downcast_suspense_boundary_from_scope(
-                            &vdom.runtime(),
-                            scope,
-                        )
-                        .filter(|s| s.has_suspended_tasks())
-                        .is_some();
-                    if is_suspense_boundary {
-                        let mount = stream.render_placeholder(
-                            |to| {
-                                {
-                                    pending_suspense_boundaries_stack
-                                        .write()
-                                        .unwrap()
-                                        .push(scope);
-                                }
-                                let out = renderer.render_scope(to, vdom, scope);
-                                {
-                                    pending_suspense_boundaries_stack.write().unwrap().pop();
-                                }
-                                out
-                            },
-                            &mut *to,
-                        )?;
-                        // Add the suspense boundary to the list of pending suspense boundaries
-                        // We will replace the mount with the resolved contents later once the suspense boundary is resolved
-                        let mut scope_to_mount_mapping_write =
-                            scope_to_mount_mapping.write().unwrap();
-                        scope_to_mount_mapping_write.insert(
-                            scope,
-                            PendingSuspenseBoundary {
-                                mount,
-                                children: vec![],
-                            },
-                        );
-                        // Add the scope to the list of children of the parent suspense boundary
-                        let pending_suspense_boundaries_stack =
-                            pending_suspense_boundaries_stack.read().unwrap();
-                        // If there is a parent suspense boundary, add the scope to the list of children
-                        // This suspense boundary will start capturing errors when the parent is resolved
-                        if let Some(parent) = pending_suspense_boundaries_stack.last() {
-                            let parent = scope_to_mount_mapping_write.get_mut(parent).unwrap();
-                            parent.children.push(scope);
-                        }
-                        // Otherwise this is a root suspense boundary, so we need to start capturing errors immediately
-                        else {
-                            vdom.in_runtime(|| {
-                                start_capturing_errors(scope);
-                            });
-                        }
-                    } else {
-                        renderer.render_scope(to, vdom, scope)?
-                    }
-                    Ok(())
-                });
+                renderer.set_render_components(streaming_render_component_callback(
+                    stream,
+                    scope_to_mount_mapping,
+                ));
             }
 
             macro_rules! throw_error {
@@ -270,6 +217,12 @@ impl SsrRendererPool {
                 };
             }
 
+            // If streaming is disabled, wait for the virtual dom to finish all suspense work
+            // before rendering anything
+            if streaming_mode == StreamingMode::Disabled {
+                virtual_dom.wait_for_suspense().await;
+            }
+
             // Render the initial frame with loading placeholders
             let mut initial_frame = renderer.render(&virtual_dom);
 
@@ -380,6 +333,74 @@ impl SsrRendererPool {
     }
 }
 
+/// Create the streaming render component callback. It will keep track of what scopes are mounted to what pending
+/// suspense boundaries in the DOM.
+///
+/// This mapping is used to replace the DOM mount with the resolved contents once the suspense boundary is finished.
+fn streaming_render_component_callback(
+    stream: Arc<StreamingRenderer<IncrementalRendererError>>,
+    scope_to_mount_mapping: Arc<RwLock<HashMap<ScopeId, PendingSuspenseBoundary>>>,
+) -> impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
+       + Send
+       + Sync
+       + 'static {
+    // We use a stack to keep track of what suspense boundaries we are nested in to add children to the correct boundary
+    // The stack starts with the root scope because the root is a suspense boundary
+    let pending_suspense_boundaries_stack = RwLock::new(vec![]);
+    move |renderer, to, vdom, scope| {
+        let is_suspense_boundary =
+            SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope)
+                .filter(|s| s.has_suspended_tasks())
+                .is_some();
+        if is_suspense_boundary {
+            let mount = stream.render_placeholder(
+                |to| {
+                    {
+                        pending_suspense_boundaries_stack
+                            .write()
+                            .unwrap()
+                            .push(scope);
+                    }
+                    let out = renderer.render_scope(to, vdom, scope);
+                    {
+                        pending_suspense_boundaries_stack.write().unwrap().pop();
+                    }
+                    out
+                },
+                &mut *to,
+            )?;
+            // Add the suspense boundary to the list of pending suspense boundaries
+            // We will replace the mount with the resolved contents later once the suspense boundary is resolved
+            let mut scope_to_mount_mapping_write = scope_to_mount_mapping.write().unwrap();
+            scope_to_mount_mapping_write.insert(
+                scope,
+                PendingSuspenseBoundary {
+                    mount,
+                    children: vec![],
+                },
+            );
+            // Add the scope to the list of children of the parent suspense boundary
+            let pending_suspense_boundaries_stack =
+                pending_suspense_boundaries_stack.read().unwrap();
+            // If there is a parent suspense boundary, add the scope to the list of children
+            // This suspense boundary will start capturing errors when the parent is resolved
+            if let Some(parent) = pending_suspense_boundaries_stack.last() {
+                let parent = scope_to_mount_mapping_write.get_mut(parent).unwrap();
+                parent.children.push(scope);
+            }
+            // Otherwise this is a root suspense boundary, so we need to start capturing errors immediately
+            else {
+                vdom.in_runtime(|| {
+                    start_capturing_errors(scope);
+                });
+            }
+        } else {
+            renderer.render_scope(to, vdom, scope)?
+        }
+        Ok(())
+    }
+}
+
 /// Start capturing errors at a suspense boundary. If the parent suspense boundary is frozen, we need to capture the errors in the suspense boundary
 /// and send them to the client to continue bubbling up
 fn start_capturing_errors(suspense_scope: ScopeId) {

+ 49 - 0
packages/fullstack/src/serve_config.rs

@@ -17,6 +17,7 @@ pub struct ServeConfigBuilder {
     pub(crate) index_path: Option<PathBuf>,
     pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
     pub(crate) context_providers: ContextProviders,
+    pub(crate) streaming_mode: StreamingMode,
 }
 
 impl LaunchConfig for ServeConfigBuilder {}
@@ -30,6 +31,7 @@ impl ServeConfigBuilder {
             index_path: None,
             incremental: None,
             context_providers: Default::default(),
+            streaming_mode: StreamingMode::default(),
         }
     }
 
@@ -141,6 +143,40 @@ impl ServeConfigBuilder {
         self
     }
 
+    /// Set the streaming mode for the server. By default, streaming is disabled.
+    ///
+    /// ```rust, no_run
+    /// # use dioxus::prelude::*;
+    /// # fn app() -> Element { todo!() }
+    /// dioxus::LaunchBuilder::new()
+    ///     .with_context(server_only! {
+    ///         dioxus::fullstack::ServeConfig::builder().streaming_mode(dioxus::fullstack::StreamingMode::OutOfOrder)
+    ///     })
+    ///     .launch(app);
+    /// ```
+    pub fn streaming_mode(mut self, mode: StreamingMode) -> Self {
+        self.streaming_mode = mode;
+        self
+    }
+
+    /// Enable out of order streaming. This will cause server futures to be resolved out of order and streamed to the client as they resolve.
+    ///
+    /// It is equivalent to calling `streaming_mode(StreamingMode::OutOfOrder)`
+    ///
+    /// /// ```rust, no_run
+    /// # use dioxus::prelude::*;
+    /// # fn app() -> Element { todo!() }
+    /// dioxus::LaunchBuilder::new()
+    ///     .with_context(server_only! {
+    ///         dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
+    ///     })
+    ///     .launch(app);
+    /// ```
+    pub fn enable_out_of_order_streaming(mut self) -> Self {
+        self.streaming_mode = StreamingMode::OutOfOrder;
+        self
+    }
+
     /// Build the ServeConfig. This may fail if the index.html file is not found.
     pub fn build(self) -> Result<ServeConfig, UnableToLoadIndex> {
         // The CLI always bundles static assets into the exe/public directory
@@ -164,6 +200,7 @@ impl ServeConfigBuilder {
             index,
             incremental: self.incremental,
             context_providers: self.context_providers,
+            streaming_mode: self.streaming_mode,
         })
     }
 }
@@ -262,6 +299,17 @@ pub(crate) struct IndexHtml {
     pub(crate) after_closing_body_tag: String,
 }
 
+/// The streaming mode to use while rendering the page
+#[derive(Clone, Copy, Default, PartialEq)]
+pub enum StreamingMode {
+    /// Streaming is disabled; all server futures should be resolved before hydrating the page on the client
+    #[default]
+    Disabled,
+    /// Out of order streaming is enabled; server futures are resolved out of order and streamed to the client
+    /// as they resolve
+    OutOfOrder,
+}
+
 /// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
 /// See [`ServeConfigBuilder`] to create a ServeConfig
 #[derive(Clone)]
@@ -269,6 +317,7 @@ pub struct ServeConfig {
     pub(crate) index: IndexHtml,
     pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
     pub(crate) context_providers: ContextProviders,
+    pub(crate) streaming_mode: StreamingMode,
 }
 
 impl LaunchConfig for ServeConfig {}

+ 3 - 0
packages/playwright-tests/fullstack/src/main.rs

@@ -9,6 +9,9 @@ use dioxus::{prelude::*, CapturedError};
 
 fn main() {
     dioxus::LaunchBuilder::new()
+        .with_cfg(server_only! {
+            dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
+        })
         .with_context(1234u32)
         .launch(app);
 }

+ 5 - 1
packages/playwright-tests/nested-suspense/src/main.rs

@@ -12,7 +12,11 @@ use dioxus::prelude::*;
 use serde::{Deserialize, Serialize};
 
 fn main() {
-    dioxus::launch(app);
+    LaunchBuilder::new()
+        .with_cfg(server_only! {
+            dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
+        })
+        .launch(app);
 }
 
 fn app() -> Element {

+ 5 - 1
packages/playwright-tests/suspense-carousel/src/main.rs

@@ -107,5 +107,9 @@ fn NestedSuspendedComponent(id: i32) -> Element {
 }
 
 fn main() {
-    dioxus::launch(app);
+    LaunchBuilder::new()
+        .with_cfg(server_only! {
+            dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming()
+        })
+        .launch(app);
 }