Streaming
For some sites, it is extremely important to optimize "time-to-first-byte". Users want to see results as soon as possible, even if not all results are ready immediately.
Dioxus supports this usecase with a technology called "HTML Streaming". HTML streaming allows you to quickly send an initial skeleton of the page to the user and then fill in various components as their data loads.
What is Streaming?
The default rendering mode in dioxus fullstack waits for all suspense boundaries to resolve before sending the entire page as HTML to the client. If you have a page with multiple chunks of async data, the server will wait for all of them to complete before rendering the page.
When streaming is enabled, the server can send chunks of HTML to the client as soon as each suspense boundary resolves. You can start interacting with a page as soon as the first part of the HTML is sent, instead of waiting for the entire page to be ready. This can lead to a much faster initial load time.
Bellow is the same hackernews example rendered with and without streaming enabled. While both pages take the same amount of time to load all the data, the page with streaming enabled on the left shows you the data as soon as it becomes available.
SEO and No JS
When streaming is enabled, all of the contents of the page are still rendered into the html document, so search engines can still crawl and index the full content of the page. However, the content will not be visible to users unless they have JavaScript enabled. If you want to support users without JavaScript, you will need to disable streaming and use the default rendering mode.
Do You Need Streaming?
HTML streaming is best suited for apps like e-commerce sites where much of the data is quick to render (the product image, description, etc) but some data takes much longer to resolve. In these cases, you don't want to make the user wait too long for the page to load, so you send down what you have as soon as possible.
Streaming adds some slight overhead and complexity to your app, so it's disabled by default.
Enabling Streaming
You can enable streaming in the ServeConfig builder with the enable_out_of_order_streaming method. If you are launching your application through the dioxus::LaunchBuilder, you can use the with_cfg method to pass in a configuration that enables streaming:
pub fn main() { dioxus::LaunchBuilder::new() .with_cfg(server_only! { dioxus::server::ServeConfig::builder().enable_out_of_order_streaming() }) .launch(app); }
or if you are using a custom axum server, you can pass the config into serve_dioxus_application directly:
#[cfg(feature = "server")] #[tokio::main] async fn main() { let addr = dioxus::cli_config::fullstack_address_or_localhost(); let router = axum::Router::new() // Server side render the application, serve static assets, and register server functions .serve_dioxus_application( dioxus::server::ServeConfig::builder().enable_out_of_order_streaming(), app, ) .into_make_service(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, router).await.unwrap(); }
Head elements with streaming
Head elements can only be rendered in the initial HTML chunk that contains the <head> tag. You should include all of your document::Link, document::Meta, and document::Title elements in the first part of your page if possible. If you have any head elements that are not included in the first chunk, they will be rendered by the client after hydration instead, which will not be visible to any search engines or users without JavaScript enabled.
The initial chunk of HTML is send after commit_initial_chunk is called for the first time. If you are using the router, this will happen automatically when all suspense boundaries above the router are resolved. If you are not using the router, you can call commit_initial_chunk manually after all of your blocking head elements have been rendered.
/// An enum of all of the possible routes in the app. #[derive(Routable, Clone)] enum Route { // The home page is at the / route #[route("/")] Home, } fn Home() -> Element { let title = use_server_future(get_title)?; let description = use_server_future(get_description)?; rsx! { // This will be rendered on the server because it is inside the same (root) // suspense boundary as the `Router` component. document::Title { {title} } SuspenseBoundary { fallback: |_| { rsx! { "Loading..." } }, AsyncHead {} } } } fn AsyncHead() -> Element { let description = use_server_future(get_description)?; // The resource should always be resolved at this point because the `?` above bubbles // up the async case if it is pending let current_description = description.read_unchecked(); let current_description = current_description.as_ref().unwrap(); rsx! { // This will be rendered on the client because it is in a // suspense boundary below the `Router` component. document::Meta { name: "description", content: "{current_description}" } } }
Response Headers with Streaming
When rendering an app with streaming enabled, Dioxus will wait for the app to commit its initial skeleton before sending a response to the user's request. This is done with the commit_initial_chunk() method.
Once the initial chunk is committed, you can no longer change the headers of the response nor change the HTTP status.
For example, you might have a server function that throws a 404 status code:
#[get("/api/post/{id}")] async fn get_post(id: u32) -> Result<String, HttpError> { match id { 1 => Ok("first post".to_string()), _ => HttpError::not_found("Post not found")?, } }
With streaming disabled, if this status code is bubbled to the root component as an error, the user will get a 404 NOT FOUND status in the response.
#[component] fn Post(id: ReadSignal<u32>) -> Element { // If `get_post` returns a 404, then the user will also get a 404 let post_data = use_loader(move || get_post(id()))?; rsx! { h1 { "Post {id}" } p { "{post_data}" } } }
However, when streaming is enabled, the status code from this server function will only be propagated to the user before the call to commit_initial_chunk().
Normally, you won't call commit_initial_chunk() yourself since the Router component calls it for you once the root suspense boundary is resolved.
This means that, when suspense is enabled, server functions won't set the HTTP status code if they are called from within a dedicated suspense boundary:
fn Home() -> Element { rsx! { SuspenseBoundary { fallback: |_| rsx! { "loading..." }, // Errors here won't propagate to the response headers Post { id: 123 } } } }