Server-Side Rendering (SSR) involves rendering the HTML content of a webpage-side and then sending the fully-rendered page to the browser. The browser only needs to display the page without additional rendering.
The main advantages are:
Developers might consider using SSR to render pages in the following scenarios:
In Modern.js, SSR is also available out of the box. Developers don't need to write complex server-side logic or worry about SSR maintenance or creating separate services.
Apart from the out-of-the-box SSR service, Modern.js also offers:
Enabling SSR in Modern.js is straightforward. Simply set server.ssr
to true
:
If you're unfamiliar with how to use Data Loader or the concept of Client Loader, please read Data Fetching first.
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:
In components, you can use hook APIs to access the data returned by the loader
function:
This feature requires version x.36.0 or above. We recommend using the latest version of the framework.
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:
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.
To use Client Loader, there must be a corresponding Server Loader, and the Server Loader must be in a .data
file, not a .loader
file.
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:
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.
If the Data Loader executes correctly but the component rendering fails, SSR rendering will partially or completely fail, as shown in the following code:
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.
The behavior of component rendering errors is unaffected by loaderFailureMode
and will not execute the Client Loader on the browser side.
Coming soon
Modern.js has built-in caching capabilities. Refer to Rendering Cache for details.
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:
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:
In such cases, you can use Modern.js built-in environment variables MODERN_TARGET
to remove unused code during the build:
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:
For more information, see Environment Variables.
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:
Directly referencing this in a component will cause CSR to throw errors, even if you use environment variables to conditionally load the code. The side effects in the dependency will still execute.
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:
Import ./a
in the file, and the SSR bundle will prioritize the .server.ts
files, while the CSR bundle will prioritize the .ts
files.
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:
After starting the application and visiting the page, you will notice a warning in the browser console:
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.
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:
Wrap elements that should not be server-side rendered with the NoSSR
component:
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 useRuntimeContext
, allowing components to access complete request information and maintaining SSR and CSR rendering consistency.
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:
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.