logo
  • Guide
  • Config
  • Plugin
  • API
  • Examples
  • Community
  • Modern.js 2.x Docs
  • English
    • 简体中文
    • English
    • Start
      Introduction
      Quick Start
      Upgrading
      Glossary
      Tech Stack
      Core Concept
      Page Entry
      Build Engine
      Web Server
      Basic Features
      Routes
      Routing
      Config Routes
      Data Solution
      Data Fetching
      Data Writing
      Data Caching
      Rendering
      Server-Side Rendering
      Streaming SSR
      Rendering Cache
      Static Site Generation
      Render Preprocessing
      Styling
      Styling
      Use CSS Modules
      Using CSS-in-JS
      Using Tailwind CSS
      HTML Template
      Import Static Assets
      Import JSON Files
      Import SVG Assets
      Import Wasm Assets
      Debug
      Data Mocking
      Network Proxy
      Using Rsdoctor
      Using Storybook
      Testing
      Playwright
      Vitest
      Jest
      Cypress
      Path Alias
      Environment Variables
      Output Files
      Deploy Application
      Advanced Features
      Using Rspack
      Using BFF
      Basic Usage
      Runtime Framework
      Extend BFF Server
      Extend Request SDK
      File Upload
      Cross-Project Invocation
      Optimize Page Performance
      Code Splitting
      Inline Static Assets
      Bundle Size Optimization
      React Compiler
      Improve Build Performance
      Browser Compatibility
      Low-Level Tools
      Source Code Build Mode
      Server Monitor
      Monitors
      Logs Events
      Metrics Events
      Internationalization
      Basic Concepts
      Quick Start
      Configuration
      Locale Detection
      Resource Loading
      Routing Integration
      API Reference
      Advanced Usage
      Best Practices
      Custom Web Server
      Topic Detail
      Module Federation
      Introduction
      Getting Started
      Application-Level Modules
      Server-Side Rendering
      Deployment
      Integrating Internationalization
      FAQ
      Dependencies FAQ
      CLI FAQ
      Build FAQ
      HMR FAQ
      Deprecated
      📝 Edit this page
      Previous pageServer-Side RenderingNext pageRendering Cache

      #Streaming SSR

      Streaming SSR is a new rendering method that updates the page content in real-time as users interact with it, enhancing user experience.

      In conventional rendering, the page is rendered all at once. In streaming rendering, the page is rendered progressively, loading data step-by-step as users interact with the page instead of loading all data at once.

      Compared to traditional rendering:

      • Faster Perceived Speed: Streaming rendering can progressively display content, quickly rendering the home page.
      • Enhanced User Experience: Users can see page content faster and interact without waiting for the entire page to render.
      • Better Performance Control: Developers can better control the loading priority and order, optimizing performance and user experience.
      • Better Adaptability: Streaming rendering adapts better to various network speeds and device performance, ensuring good performance across different environments.

      #Enabling Streaming Rendering

      Modern.js supports React 18's streaming rendering, which can be enabled as follows:

      modern.config.ts
      import { defineConfig } from '@modern-js/app-tools';
      
      export default defineConfig({
        server: {
          ssr: {
            mode: 'stream',
          },
        },
      });

      Modern.js streaming rendering is based on React Router and involves several key APIs:

      • defer: Used in Data Loader to support asynchronous data fetching.
      • Await: Used to render the asynchronous data returned by the Data Loader.
      • useAsyncValue: Used to fetch data from the nearest parent Await component.

      #Fetching Data

      user/[id]/page.data.ts
      import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';
      
      interface User {
        name: string;
        age: number;
      }
      
      export interface Data {
        data: User;
      }
      
      export const loader = ({ params }: LoaderFunctionArgs) => {
        const userId = params.id;
      
        const user = new Promise<User>(resolve => {
          setTimeout(() => {
            resolve({
              name: `user-${userId}`,
              age: 18,
            });
          }, 200);
        });
      
        return defer({ data: user });
      };

      Here, user is a Promise object representing asynchronously fetched data, processed using defer. Notice that defer must receive an object parameter; a direct Promise cannot be passed.

      Additionally, defer can receive both asynchronous and synchronous data. In the example below, short-duration requests are returned using object data, while longer-duration requests are returned using a Promise:

      user/[id]/page.data.ts
      export const loader = async ({ params }: LoaderFunctionArgs) => {
        const userId = params.id;
      
        const user = new Promise<User>(resolve => {
          setTimeout(() => {
            resolve({
              name: `user-${userId}`,
              age: 18,
            });
          }, 2000);
        });
      
        const otherData = new Promise<string>(resolve => {
          setTimeout(() => {
            resolve('some sync data');
          }, 200);
        });
      
        return defer({
          data: user,
          other: await otherData,
        });
      };

      This way, the application can prioritize displaying partially available content without waiting for the most time-consuming data requests.

      #Rendering Data

      To render the asynchronous data returned by the Data Loader, use the Await component. For example:

      user/[id]/page.tsx
      import { Await, useLoaderData } from '@modern-js/runtime/router';
      import { Suspense } from 'react';
      import type { Data } from './page.data';
      
      const Page = () => {
        const data = useLoaderData() as Data;
      
        return (
          <div>
            User info:
            <Suspense fallback={<div id="loading">loading user data ...</div>}>
              <Await resolve={data.data}>
                {user => {
                  return (
                    <div id="data">
                      name: {user.name}, age: {user.age}
                    </div>
                  );
                }}
              </Await>
            </Suspense>
          </div>
        );
      };
      
      export default Page;

      The Await component needs to be wrapped inside a Suspense component. The resolve prop of Await should be the asynchronously fetched data from the Data Loader. When the data is fetched, it will be rendered using the Render Props pattern. During data fetching, the content set by the fallback prop of Suspense is displayed.

      Warning

      When importing types from the page.data.ts file, use import type to ensure only type information is imported, preventing Data Loader code from being bundled into the frontend.

      In the component, you can also fetch asynchronous data returned by the Data Loader using useAsyncValue. For example:

      page.tsx
      import { useAsyncValue } from '@modern-js/runtime/router';
      
      const UserInfo = () => {
        const user = useAsyncValue();
        return (
          <div>
            name: {user.name}, age: {user.age}
          </div>
        );
      };
      
      const Page = () => {
        const data = useLoaderData() as Data;
        return (
          <div>
            User info:
            <Suspense fallback={<div id="loading">loading user data ...</div>}>
              <Await resolve={data.data}>
                <UserInfo />
              </Await>
            </Suspense>
          </div>
        );
      };
      
      export default Page;

      #Error Handling

      The errorElement prop of the Await component handles errors in Data Loader or sub-component rendering. For example, intentionally throwing an error in the Data Loader function:

      page.loader.ts
      import { defer } from '@modern-js/runtime/router';
      
      export default () => {
        const data = new Promise((resolve, reject) => {
          setTimeout(() => {
            reject(new Error('error occurs'));
          }, 200);
        });
      
        return defer({ data });
      };

      Then, fetch the error using useAsyncError and set a component to render the error message for the errorElement prop of the Await component:

      page.ts
      import { Await, useAsyncError, useLoaderData } from '@modern-js/runtime/router';
      import { Suspense } from 'react';
      
      export default function Page() {
        const data = useLoaderData();
      
        return (
          <div>
            Error page
            <Suspense fallback={<div>loading ...</div>}>
              <Await resolve={data.data} errorElement={<ErrorElement />}>
                {(data: any) => {
                  return <div>never displayed</div>;
                }}
              </Await>
            </Suspense>
          </div>
        );
      }
      
      function ErrorElement() {
        const error = useAsyncError() as Error;
        return <p>Something went wrong! {error.message}</p>;
      }

      #Waiting for All Content to Load for Crawlers

      Streaming can enhance user experience by allowing users to perceive content as it becomes available.

      However, when a crawler visits the page, it might need to load all content and output the entire HTML at once, rather than progressively loading it.

      Modern.js uses isbot to determine if a request is from a crawler based on the user-agent header.

      #Related Documentation

      1. Deferred Data
      2. New Suspense SSR Architecture in React 18