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
      Rendering Mode Overview
      Server-Side Rendering
      Streaming Server-Side Rendering
      Rendering Cache
      Static Site Generation
      React Server Components (RSC)
      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
      Creating Extensible BFF Functions
      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
      📝 Edit this page
      Previous pageRendering Mode OverviewNext pageStreaming Server-Side Rendering

      #Server-Side Rendering

      Server-Side Rendering (SSR) generates complete HTML pages on the server and sends them to the browser for direct display, without requiring additional client-side rendering.

      #Core Advantages

      • Faster First Screen: Server-side pre-rendering allows the browser to display content directly without waiting for JavaScript execution
      • Better SEO: Search engines can directly index complete HTML content
      • Out of the Box: No need to write complex server-side logic or maintain separate services
      Default Rendering Mode

      Modern.js SSR uses streaming rendering (Streaming SSR) by default, allowing pages to be returned progressively as they render, so users can see initial content faster.

      For detailed usage, refer to the Streaming SSR documentation.

      To switch to traditional SSR mode (waiting for all data to load before returning at once), you can configure:

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

      #Enabling SSR

      Enabling SSR in Modern.js is straightforward. Simply set server.ssr to true:

      modern.config.ts
      import { defineConfig } from '@modern-js/app-tools';
      
      export default defineConfig({
        server: {
          ssr: true, // Streaming rendering enabled by default
        },
      });

      #Data Fetching

      Modern.js provides Data Loader, enabling developers to fetch data isomorphically under both SSR and CSR. Each route module (such as layout.tsx and page.tsx) can define its own Data Loader:

      Learn More

      The following approach will not have streaming rendering effects. To achieve streaming rendering effects, refer to Streaming SSR.

      src/routes/page.data.ts
      export const loader = () => {
        return {
          message: 'Hello World',
        };
      };

      Access data in components through Hooks API:

      import { useLoaderData } from '@modern-js/runtime/router';
      export default () => {
        const data = useLoaderData();
        return <div>{data.message}</div>;
      };

      #Using Client Loader

      By default, in SSR applications, the loader function only executes on the server. However, in some scenarios, developers might want requests made on the client-side to bypass the SSR service and directly fetch data from the source. For example:

      1. Reducing network consumption on the client-side by directly fetching from the data source.
      2. The application has data cached on the client side and doesn't want to fetch data from the SSR service.

      Modern.js supports adding a .data.client file, also named exported as loader, in SSR applications. If the Data Loader fails to execute on the server side, or when navigating on the client side, it will execute the loader function on the client side instead of sending a data request to the SSR service.

      page.data.client.ts
      import cache from 'my-cache';
      
      export async function loader({ params }) {
        if (cache.has(params.id)) {
          return cache.get(params.id);
        }
        const res = await fetch(`URL_ADDRESS?id=${params.id}`);
        const data = await res.json();
        return {
          message: data.message,
        }
      }

      #SSR Fallback

      In Modern.js, if an application encounters an error during SSR, it automatically falls back to CSR mode and re-fetches data, ensuring the page can display correctly. SSR fallback can occur for two main reasons:

      1. Data Loader execution error.
      2. React component rendering error on the server side.

      #Data Loader Execution Error

      By default, if the loader function for a route throws an error, the framework renders the <ErrorBoundary> component directly on the server, displaying the error message. This is the default behavior of most frameworks.

      Modern.js also supports customizing the fallback strategy through the loaderFailureMode field in the server.ssr configuration. Setting this field to clientRender immediately falls back to CSR mode and re-fetches the data.

      If a Client Loader is defined for the route, it will be used to re-fetch the data. If re-rendering fails again, the <ErrorBoundary> component will be displayed.

      #Component Rendering Error

      If the Data Loader executes correctly but the component rendering fails, SSR rendering will partially or completely fail, as shown in the following code:

      import { Await, useLoaderData } from '@modern-js/runtime/router';
      import { Suspense } from 'react';
      
      const Page = () => {
        const data = useLoaderData();
        const isNode = typeof window === 'undefined';
        const undefinedVars = data.unDefined;
        const definedVars = data.defined;
      
        return (
          <div>
            {isNode ? undefinedVars.msg : definedVars.msg}
          </div>
        );
      };
      
      export default Page;

      In this case, Modern.js will fallback the page to CSR and use the existing data from the Data Loader to render. If the rendering still fails, the <ErrorBoundary> component will be rendered.

      Tip

      The behavior of component rendering errors is unaffected by loaderFailureMode and will not execute the Client Loader on the browser side.

      #Logging and Monitoring

      Note

      Coming soon

      #Page Caching

      Modern.js has built-in caching capabilities. Refer to Rendering Cache for details.

      #Differences in Runtime Environment

      SSR applications run on both the server and the client, with differing Web and Node APIs.

      When enabling SSR, Modern.js uses the same entry to build both SSR and CSR bundles. Therefore, having Web APIs in the SSR bundle or Node APIs in the CSR bundle can lead to runtime errors. This usually happens in two scenarios:

      • There are issues in the application's own code.
      • The dependency package contains side effects.

      #Issues with Own Code

      This scenario often arises when migrating from CSR to SSR. CSR applications typically import Web APIs in the code. For example, an application might set up global event listeners:

      document.addEventListener('load', () => {
        console.log('document load');
      });
      const App = () => {
        return <div>Hello World</div>;
      };
      export default App;

      In such cases, you can use Modern.js built-in environment variables MODERN_TARGET to remove unused code during the build:

      if (process.env.MODERN_TARGET === 'browser') {
        document.addEventListener('load', () => {
          console.log('document load');
        });
      }

      After packing in the development environment, the SSR and CSR bundles will compile as follows. Therefore, Web API errors will not occur in the SSR environment:

      // SSR Bundle
      if (false) {
      }
      
      // CSR Bundle
      if (true) {
        document.addEventListener('load', () => {
          console.log('document load');
        });
      }
      Note

      For more information, see Environment Variables.

      #Side Effects in Dependencies

      This scenario can occur at any time in SSR applications because not all community packages support running in both environments. Some packages only need to run in one. For instance, importing package A that has a side effect using Web APIs:

      packageA
      document.addEventListener('load', () => {
        console.log('document load');
      });
      
      export const doSomething = () => {}

      Directly referencing this in a component will cause SSR to throw errors, even if you use environment variables to conditionally load the code. The side effects in the dependency will still execute.

      routes/page.tsx
      import { doSomething } from 'packageA';
      
      export const Page = () => {
        if (process.env.MODERN_TARGET === 'browser') {
          doSomething();
        }
        return <div>Hello World</div>
      }

      Modern.js supports distinguishing between SSR and CSR bundles by using .server. suffix files. You can create .ts and .server.ts files with the same name to create a proxy:

      a.ts
      export { doSomething } from 'packageA';
      a.server.ts
      export const doSomething: any = () => {};

      Import ./a in the file, and the SSR bundle will prioritize the .server.ts files, while the CSR bundle will prioritize the .ts files.

      routes/page.tsx
      import { doSomething } from './a'
      
      export const Page = () => {
        doSomething();
        return <div>Hello World</div>
      }

      #Common Issues

      #Ensuring Consistent Rendering

      In SSR applications, it is crucial to ensure that the rendering results on the server are consistent with the hydration results in the browser. Inconsistent rendering may lead to unexpected outcomes. Here’s an example demonstrating issues when SSR and CSR render differently. Add the following code to your component:

      {
        typeof window !== 'undefined' ? <div>browser content</div> : null;
      }

      After starting the application and visiting the page, you will notice a warning in the browser console:

      Warning: Expected server HTML to contain a matching <div> in <div>.

      This warning is caused by a mismatch between the React hydrate results and the SSR rendering results. Although the current page appears normal, complex applications may experience DOM hierarchy disruptions or style issues.

      Info

      For more information on React hydrate logic, refer to here.

      The application needs to maintain consistency between SSR and CSR rendering results. If inconsistencies occur, it indicates that some content should not be rendered by SSR. Modern.js provides the <NoSSR> utility component for such scenarios:

      import { NoSSR } from '@modern-js/runtime/ssr';

      Wrap elements that should not be server-side rendered with the NoSSR component:

      <NoSSR>
        <div>browser content</div>
      </NoSSR>

      After modifying the code, refresh the page and notice that the previous warning has disappeared. Open the browser's developer tools and check the Network tab. The returned HTML document will not contain the content wrapped by the NoSSR component.

      In practical scenarios, some UI displays might be affected by the user's device, such as UA information. Modern.js also provides APIs like use(RuntimeContext), allowing components to access complete request information and maintaining SSR and CSR rendering consistency. For detailed usage, please refer to Runtime Context.

      #Attention to Memory Leaks

      Alert

      In SSR scenarios, developers need to pay special attention to memory leaks. Even minor memory leaks can significantly impact services after many requests.

      With SSR, each browser request triggers server-side component rendering. Therefore, you should avoid defining any data structures that continually grow globally, subscribing to global events, or creating non-disposable streams.

      For example, when using redux-observable, developers accustomed to CSR might code as follows:

      /* Code is just an example and not executable */
      import { createEpicMiddleware, combineEpics } from 'redux-observable';
      
      const epicMiddleware = createEpicMiddleware();
      const rootEpic = combineEpics();
      
      export default function Test() {
        epicMiddleware.run(rootEpic);
        return <div>Hello Modern.js</div>;
      }

      In this case, the epicMiddleware instance is created outside the component, and epicMiddleware.run is called within the component.

      This code does not cause issues on the client-side. However, in SSR, the Middleware instance remains non-disposable. Each time the component renders, calling epicMiddleware.run(rootEpic) adds new event bindings internally, causing the entire object to grow continuously, ultimately affecting application performance.

      Such issues are not easily noticed in CSR. When transitioning from CSR to SSR, if you're unsure whether your application has such hidden pitfalls, consider stress testing the application.